Proyectos de Subversion Moodle

Rev

Rev 1 | | 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
/**
18
 * External course functions unit tests
19
 *
20
 * @package    core_course
21
 * @category   external
22
 * @copyright  2012 Jerome Mouneyrac
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
use \core_external\external_api;
27
 
28
defined('MOODLE_INTERNAL') || die();
29
 
30
global $CFG;
31
 
32
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
33
 
34
/**
35
 * External course functions unit tests
36
 *
37
 * @package    core_course
38
 * @category   external
39
 * @copyright  2012 Jerome Mouneyrac
40
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 */
11 efrain 42
final class externallib_test extends externallib_advanced_testcase {
1 efrain 43
 
44
    /**
45
     * Tests set up
46
     */
47
    protected function setUp(): void {
48
        global $CFG;
49
        require_once($CFG->dirroot . '/course/externallib.php');
50
    }
51
 
52
    /**
53
     * Test create_categories
54
     */
11 efrain 55
    public function test_create_categories(): void {
1 efrain 56
 
57
        global $DB;
58
 
59
        $this->resetAfterTest(true);
60
 
61
        // Set the required capabilities by the external function
62
        $contextid = context_system::instance()->id;
63
        $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
64
 
65
        // Create base categories.
66
        $category1 = new stdClass();
67
        $category1->name = 'Root Test Category 1';
68
        $category2 = new stdClass();
69
        $category2->name = 'Root Test Category 2';
70
        $category2->idnumber = 'rootcattest2';
71
        $category2->desc = 'Description for root test category 1';
72
        $category2->theme = 'classic';
73
        $categories = array(
74
            array('name' => $category1->name, 'parent' => 0),
75
            array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber,
76
                'description' => $category2->desc, 'theme' => $category2->theme)
77
        );
78
 
79
        $createdcats = core_course_external::create_categories($categories);
80
 
81
        // We need to execute the return values cleaning process to simulate the web service server.
82
        $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats);
83
 
84
        // Initially confirm that base data was inserted correctly.
85
        $this->assertEquals($category1->name, $createdcats[0]['name']);
86
        $this->assertEquals($category2->name, $createdcats[1]['name']);
87
 
88
        // Save the ids.
89
        $category1->id = $createdcats[0]['id'];
90
        $category2->id = $createdcats[1]['id'];
91
 
92
        // Create on sub category.
93
        $category3 = new stdClass();
94
        $category3->name = 'Sub Root Test Category 3';
95
        $subcategories = array(
96
            array('name' => $category3->name, 'parent' => $category1->id)
97
        );
98
 
99
        $createdsubcats = core_course_external::create_categories($subcategories);
100
 
101
        // We need to execute the return values cleaning process to simulate the web service server.
102
        $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats);
103
 
104
        // Confirm that sub categories were inserted correctly.
105
        $this->assertEquals($category3->name, $createdsubcats[0]['name']);
106
 
107
        // Save the ids.
108
        $category3->id = $createdsubcats[0]['id'];
109
 
110
        // Calling the ws function should provide a new sortorder to give category1,
111
        // category2, category3. New course categories are ordered by id not name.
112
        $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
113
        $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
114
        $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
115
 
116
        // sortorder sequence (and sortorder) must be:
117
        // category 1
118
        //   category 3
119
        // category 2
120
        $this->assertGreaterThan($category1->sortorder, $category3->sortorder);
121
        $this->assertGreaterThan($category3->sortorder, $category2->sortorder);
122
 
123
        // Call without required capability
124
        $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
125
        $this->expectException('required_capability_exception');
126
        $createdsubcats = core_course_external::create_categories($subcategories);
127
 
128
    }
129
 
130
    /**
131
     * Test delete categories
132
     */
11 efrain 133
    public function test_delete_categories(): void {
1 efrain 134
        global $DB;
135
 
136
        $this->resetAfterTest(true);
137
 
138
        // Set the required capabilities by the external function
139
        $contextid = context_system::instance()->id;
140
        $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
141
 
142
        $category1  = self::getDataGenerator()->create_category();
143
        $category2  = self::getDataGenerator()->create_category(
144
                array('parent' => $category1->id));
145
        $category3  = self::getDataGenerator()->create_category();
146
        $category4  = self::getDataGenerator()->create_category(
147
                array('parent' => $category3->id));
148
        $category5  = self::getDataGenerator()->create_category(
149
                array('parent' => $category4->id));
150
 
151
        //delete category 1 and 2 + delete category 4, category 5 moved under category 3
152
        core_course_external::delete_categories(array(
153
            array('id' => $category1->id, 'recursive' => 1),
154
            array('id' => $category4->id)
155
        ));
156
 
157
        //check $category 1 and 2 are deleted
158
        $notdeletedcount = $DB->count_records_select('course_categories',
159
            'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')');
160
        $this->assertEquals(0, $notdeletedcount);
161
 
162
        //check that $category5 as $category3 for parent
163
        $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id));
164
        $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id);
165
 
166
         // Call without required capability
167
        $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
168
        $this->expectException('required_capability_exception');
169
        $createdsubcats = core_course_external::delete_categories(
170
                array(array('id' => $category3->id)));
171
    }
172
 
173
    /**
174
     * Test get categories
175
     */
11 efrain 176
    public function test_get_categories(): void {
1 efrain 177
        global $DB;
178
 
179
        $this->resetAfterTest(true);
180
 
181
        $generatedcats = array();
182
        $category1data['idnumber'] = 'idnumbercat1';
183
        $category1data['name'] = 'Category 1 for PHPunit test';
184
        $category1data['description'] = 'Category 1 description';
185
        $category1data['descriptionformat'] = FORMAT_MOODLE;
186
        $category1  = self::getDataGenerator()->create_category($category1data);
187
        $generatedcats[$category1->id] = $category1;
188
        $category2  = self::getDataGenerator()->create_category(
189
                array('parent' => $category1->id));
190
        $generatedcats[$category2->id] = $category2;
191
        $category6  = self::getDataGenerator()->create_category(
192
                array('parent' => $category1->id, 'visible' => 0));
193
        $generatedcats[$category6->id] = $category6;
194
        $category3  = self::getDataGenerator()->create_category();
195
        $generatedcats[$category3->id] = $category3;
196
        $category4  = self::getDataGenerator()->create_category(
197
                array('parent' => $category3->id));
198
        $generatedcats[$category4->id] = $category4;
199
        $category5  = self::getDataGenerator()->create_category(
200
                array('parent' => $category4->id));
201
        $generatedcats[$category5->id] = $category5;
202
 
203
        // Set the required capabilities by the external function.
204
        $context = context_system::instance();
205
        $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
206
        $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
207
 
208
        // Retrieve category1 + sub-categories except not visible ones
209
        $categories = core_course_external::get_categories(array(
210
            array('key' => 'id', 'value' => $category1->id),
211
            array('key' => 'visible', 'value' => 1)), 1);
212
 
213
        // We need to execute the return values cleaning process to simulate the web service server.
214
        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
215
 
216
        // Check we retrieve the good total number of categories.
217
        $this->assertEquals(2, count($categories));
218
 
219
        // Check the return values
220
        foreach ($categories as $category) {
221
            $generatedcat = $generatedcats[$category['id']];
222
            $this->assertEquals($category['idnumber'], $generatedcat->idnumber);
223
            $this->assertEquals($category['name'], $generatedcat->name);
224
            // Description was converted to the HTML format.
225
            $this->assertEquals($category['description'], format_text($generatedcat->description, FORMAT_MOODLE, array('para' => false)));
226
            $this->assertEquals($category['descriptionformat'], FORMAT_HTML);
227
        }
228
 
229
        // Check categories by ids.
230
        $ids = implode(',', array_keys($generatedcats));
231
        $categories = core_course_external::get_categories(array(
232
            array('key' => 'ids', 'value' => $ids)), 0);
233
 
234
        // We need to execute the return values cleaning process to simulate the web service server.
235
        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
236
 
237
        // Check we retrieve the good total number of categories.
238
        $this->assertEquals(6, count($categories));
239
        // Check ids.
240
        $returnedids = [];
241
        foreach ($categories as $category) {
242
            $returnedids[] = $category['id'];
243
        }
244
        // Sort the arrays upon comparision.
245
        $this->assertEqualsCanonicalizing(array_keys($generatedcats), $returnedids);
246
 
247
        // Check different params.
248
        $categories = core_course_external::get_categories(array(
249
            array('key' => 'id', 'value' => $category1->id),
250
            array('key' => 'ids', 'value' => $category1->id),
251
            array('key' => 'idnumber', 'value' => $category1->idnumber),
252
            array('key' => 'visible', 'value' => 1)), 0);
253
 
254
        // We need to execute the return values cleaning process to simulate the web service server.
255
        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
256
 
257
        $this->assertEquals(1, count($categories));
258
 
259
        // Same query, but forcing a parameters clean.
260
        $categories = core_course_external::get_categories(array(
261
            array('key' => 'id', 'value' => "$category1->id"),
262
            array('key' => 'idnumber', 'value' => $category1->idnumber),
263
            array('key' => 'name', 'value' => $category1->name . "<br/>"),
264
            array('key' => 'visible', 'value' => '1')), 0);
265
        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
266
 
267
        $this->assertEquals(1, count($categories));
268
 
269
        // Retrieve categories from parent.
270
        $categories = core_course_external::get_categories(array(
271
            array('key' => 'parent', 'value' => $category3->id)), 1);
272
        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
273
 
274
        $this->assertEquals(2, count($categories));
275
 
276
        // Retrieve all categories.
277
        $categories = core_course_external::get_categories();
278
 
279
        // We need to execute the return values cleaning process to simulate the web service server.
280
        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
281
 
282
        $this->assertEquals($DB->count_records('course_categories'), count($categories));
283
 
284
        $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
285
 
286
        // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
287
        // It should retrieve all visible categories as well.
288
        set_config('maxcategorydepth', 2);
289
        $categories = core_course_external::get_categories();
290
 
291
        // We need to execute the return values cleaning process to simulate the web service server.
292
        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
293
 
294
        $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
295
 
296
        // Call without required capability (it will fail cause of the search on idnumber).
297
        $this->expectException('moodle_exception');
298
        $categories = core_course_external::get_categories(array(
299
            array('key' => 'id', 'value' => $category1->id),
300
            array('key' => 'idnumber', 'value' => $category1->idnumber),
301
            array('key' => 'visible', 'value' => 1)), 0);
302
    }
303
 
304
    /**
305
     * Test update_categories
306
     */
11 efrain 307
    public function test_update_categories(): void {
1 efrain 308
        global $DB;
309
 
310
        $this->resetAfterTest(true);
311
 
312
        // Set the required capabilities by the external function
313
        $contextid = context_system::instance()->id;
314
        $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
315
 
316
        // Create base categories.
317
        $category1data['idnumber'] = 'idnumbercat1';
318
        $category1data['name'] = 'Category 1 for PHPunit test';
319
        $category1data['description'] = 'Category 1 description';
320
        $category1data['descriptionformat'] = FORMAT_MOODLE;
321
        $category1  = self::getDataGenerator()->create_category($category1data);
322
        $category2  = self::getDataGenerator()->create_category(
323
                array('parent' => $category1->id));
324
        $category3  = self::getDataGenerator()->create_category();
325
        $category4  = self::getDataGenerator()->create_category(
326
                array('parent' => $category3->id));
327
        $category5  = self::getDataGenerator()->create_category(
328
                array('parent' => $category4->id));
329
 
330
        // We update all category1 attribut.
331
        // Then we move cat4 and cat5 parent: cat3 => cat1
332
        $categories = array(
333
            array('id' => $category1->id,
334
                'name' => $category1->name . '_updated',
335
                'idnumber' => $category1->idnumber . '_updated',
336
                'description' => $category1->description . '_updated',
337
                'descriptionformat' => FORMAT_HTML,
338
                'theme' => $category1->theme),
339
            array('id' => $category4->id, 'parent' => $category1->id));
340
 
341
        core_course_external::update_categories($categories);
342
 
343
        // Check the values were updated.
344
        $dbcategories = $DB->get_records_select('course_categories',
345
                'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id
346
                . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')');
347
        $this->assertEquals($category1->name . '_updated',
348
                $dbcategories[$category1->id]->name);
349
        $this->assertEquals($category1->idnumber . '_updated',
350
                $dbcategories[$category1->id]->idnumber);
351
        $this->assertEquals($category1->description . '_updated',
352
                $dbcategories[$category1->id]->description);
353
        $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat);
354
 
355
        // Check that category4 and category5 have been properly moved.
356
        $this->assertEquals('/' . $category1->id . '/' . $category4->id,
357
                $dbcategories[$category4->id]->path);
358
        $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id,
359
                $dbcategories[$category5->id]->path);
360
 
361
        // Call without required capability.
362
        $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
363
        $this->expectException('required_capability_exception');
364
        core_course_external::update_categories($categories);
365
    }
366
 
367
    /**
368
     * Test update_categories method for moving categories
369
     */
11 efrain 370
    public function test_update_categories_moving(): void {
1 efrain 371
        $this->resetAfterTest();
372
 
373
        // Create data.
374
        $categorya  = self::getDataGenerator()->create_category([
375
            'name' => 'CAT_A',
376
        ]);
377
        $categoryasub = self::getDataGenerator()->create_category([
378
            'name' => 'SUBCAT_A',
379
            'parent' => $categorya->id
380
        ]);
381
        $categoryb  = self::getDataGenerator()->create_category([
382
            'name' => 'CAT_B',
383
        ]);
384
 
385
        // Create a new test user.
386
        $testuser = self::getDataGenerator()->create_user();
387
        $this->setUser($testuser);
388
 
389
        // Set the capability for CAT_A only.
390
        $contextcata = context_coursecat::instance($categorya->id);
391
        $roleid = $this->assignUserCapability('moodle/category:manage', $contextcata->id);
392
 
393
        // Then we move SUBCAT_A parent: CAT_A => CAT_B.
394
        $categories = [
395
            [
396
                'id' => $categoryasub->id,
397
                'parent' => $categoryb->id
398
            ]
399
        ];
400
 
401
        $this->expectException('required_capability_exception');
402
        core_course_external::update_categories($categories);
403
    }
404
 
405
    /**
406
     * Test create_courses numsections
407
     */
11 efrain 408
    public function test_create_course_numsections(): void {
1 efrain 409
        global $DB;
410
 
411
        $this->resetAfterTest(true);
412
 
413
        // Set the required capabilities by the external function.
414
        $contextid = context_system::instance()->id;
415
        $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
416
        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
417
 
418
        $numsections = 10;
419
        $category  = self::getDataGenerator()->create_category();
420
 
421
        // Create base categories.
422
        $course1['fullname'] = 'Test course 1';
423
        $course1['shortname'] = 'Testcourse1';
424
        $course1['categoryid'] = $category->id;
425
        $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
426
 
427
        $courses = array($course1);
428
 
429
        $createdcourses = core_course_external::create_courses($courses);
430
        foreach ($createdcourses as $createdcourse) {
431
            $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
432
            $modinfo = get_fast_modinfo($createdcourse['id']);
433
            $sections = $modinfo->get_section_info_all();
434
            $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
435
            $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
436
        }
437
    }
438
 
439
    /**
440
     * Test create_courses
441
     */
11 efrain 442
    public function test_create_courses(): void {
1 efrain 443
        global $DB;
444
 
445
        $this->resetAfterTest(true);
446
 
447
        // Enable course completion.
448
        set_config('enablecompletion', 1);
449
        // Enable course themes.
450
        set_config('allowcoursethemes', 1);
451
 
452
        // Custom fields.
453
        $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
454
 
455
        $fieldtext = self::getDataGenerator()->create_custom_field([
456
            'categoryid' => $fieldcategory->get('id'), 'name' => 'Text', 'shortname' => 'text', 'type' => 'text',
457
        ]);
458
        $fieldtextarea = self::getDataGenerator()->create_custom_field([
459
            'categoryid' => $fieldcategory->get('id'), 'name' => 'Textarea', 'shortname' => 'textarea', 'type' => 'textarea',
460
        ]);
461
 
462
        // Set the required capabilities by the external function
463
        $contextid = context_system::instance()->id;
464
        $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
465
        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
466
        $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
467
 
468
        $category  = self::getDataGenerator()->create_category();
469
 
470
        // Create base categories.
471
        $course1['fullname'] = 'Test course 1';
472
        $course1['shortname'] = 'Testcourse1';
473
        $course1['categoryid'] = $category->id;
474
        $course2['fullname'] = 'Test course 2';
475
        $course2['shortname'] = 'Testcourse2';
476
        $course2['categoryid'] = $category->id;
477
        $course2['idnumber'] = 'testcourse2idnumber';
478
        $course2['summary'] = 'Description for course 2';
479
        $course2['summaryformat'] = FORMAT_MOODLE;
480
        $course2['format'] = 'weeks';
481
        $course2['showgrades'] = 1;
482
        $course2['newsitems'] = 3;
483
        $course2['startdate'] = 1420092000; // 01/01/2015.
484
        $course2['enddate'] = 1422669600; // 01/31/2015.
485
        $course2['numsections'] = 4;
486
        $course2['maxbytes'] = 100000;
487
        $course2['showreports'] = 1;
488
        $course2['visible'] = 0;
489
        $course2['hiddensections'] = 0;
490
        $course2['groupmode'] = 0;
491
        $course2['groupmodeforce'] = 0;
492
        $course2['defaultgroupingid'] = 0;
493
        $course2['enablecompletion'] = 1;
494
        $course2['completionnotify'] = 1;
495
        $course2['lang'] = 'en';
496
        $course2['forcetheme'] = 'classic';
497
        $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0);
498
        $course3['fullname'] = 'Test course 3';
499
        $course3['shortname'] = 'Testcourse3';
500
        $course3['categoryid'] = $category->id;
501
        $course3['format'] = 'topics';
502
        $course3options = array('numsections' => 8,
503
            'hiddensections' => 1,
504
            'coursedisplay' => 1);
505
        $course3['courseformatoptions'] = array();
506
        foreach ($course3options as $key => $value) {
507
            $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
508
        }
509
        $course4['fullname'] = 'Test course with custom fields';
510
        $course4['shortname'] = 'Testcoursecustomfields';
511
        $course4['categoryid'] = $category->id;
512
        $course4['customfields'] = [
513
            ['shortname' => $fieldtext->get('shortname'), 'value' => 'And I want to tell you so much'],
514
            ['shortname' => $fieldtextarea->get('shortname'), 'value' => 'I love you'],
515
        ];
516
        $courses = array($course4, $course1, $course2, $course3);
517
 
518
        $createdcourses = core_course_external::create_courses($courses);
519
 
520
        // We need to execute the return values cleaning process to simulate the web service server.
521
        $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
522
 
523
        // Check that right number of courses were created.
524
        $this->assertEquals(4, count($createdcourses));
525
 
526
        // Check that the courses were correctly created.
527
        foreach ($createdcourses as $createdcourse) {
528
            $courseinfo = course_get_format($createdcourse['id'])->get_course();
529
 
530
            if ($createdcourse['shortname'] == $course2['shortname']) {
531
                $this->assertEquals($courseinfo->fullname, $course2['fullname']);
532
                $this->assertEquals($courseinfo->shortname, $course2['shortname']);
533
                $this->assertEquals($courseinfo->category, $course2['categoryid']);
534
                $this->assertEquals($courseinfo->idnumber, $course2['idnumber']);
535
                $this->assertEquals($courseinfo->summary, $course2['summary']);
536
                $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']);
537
                $this->assertEquals($courseinfo->format, $course2['format']);
538
                $this->assertEquals($courseinfo->showgrades, $course2['showgrades']);
539
                $this->assertEquals($courseinfo->newsitems, $course2['newsitems']);
540
                $this->assertEquals($courseinfo->startdate, $course2['startdate']);
541
                $this->assertEquals($courseinfo->enddate, $course2['enddate']);
542
                $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']);
543
                $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']);
544
                $this->assertEquals($courseinfo->showreports, $course2['showreports']);
545
                $this->assertEquals($courseinfo->visible, $course2['visible']);
546
                $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']);
547
                $this->assertEquals($courseinfo->groupmode, $course2['groupmode']);
548
                $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']);
549
                $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']);
550
                $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']);
551
                $this->assertEquals($courseinfo->lang, $course2['lang']);
552
                $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
553
 
554
                // We enabled completion at the beginning of the test.
555
                $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
556
 
557
            } else if ($createdcourse['shortname'] == $course1['shortname']) {
558
                $courseconfig = get_config('moodlecourse');
559
                $this->assertEquals($courseinfo->fullname, $course1['fullname']);
560
                $this->assertEquals($courseinfo->shortname, $course1['shortname']);
561
                $this->assertEquals($courseinfo->category, $course1['categoryid']);
562
                $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML);
563
                $this->assertEquals($courseinfo->format, $courseconfig->format);
564
                $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades);
565
                $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems);
566
                $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes);
567
                $this->assertEquals($courseinfo->showreports, $courseconfig->showreports);
568
                $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode);
569
                $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce);
570
                $this->assertEquals($courseinfo->defaultgroupingid, 0);
571
            } else if ($createdcourse['shortname'] == $course3['shortname']) {
572
                $this->assertEquals($courseinfo->fullname, $course3['fullname']);
573
                $this->assertEquals($courseinfo->shortname, $course3['shortname']);
574
                $this->assertEquals($courseinfo->category, $course3['categoryid']);
575
                $this->assertEquals($courseinfo->format, $course3['format']);
576
                $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']);
577
                $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(),
578
                    $course3options['numsections']);
579
                $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
580
            } else if ($createdcourse['shortname'] == $course4['shortname']) {
581
                $this->assertEquals($courseinfo->fullname, $course4['fullname']);
582
                $this->assertEquals($courseinfo->shortname, $course4['shortname']);
583
                $this->assertEquals($courseinfo->category, $course4['categoryid']);
584
 
585
                $handler = core_course\customfield\course_handler::create();
586
                $customfields = $handler->export_instance_data_object($createdcourse['id']);
587
                $this->assertEquals((object) [
588
                    'text' => 'And I want to tell you so much',
589
                    'textarea' => '<div class="text_to_html">I love you</div>',
590
                ], $customfields);
591
            } else {
592
                throw new moodle_exception('Unexpected shortname');
593
            }
594
        }
595
 
596
        // Call without required capability
597
        $this->unassignUserCapability('moodle/course:create', $contextid, $roleid);
598
        $this->expectException('required_capability_exception');
599
        $createdsubcats = core_course_external::create_courses($courses);
600
    }
601
 
602
    /**
603
     * Data provider for testing empty fields produce expected exceptions
604
     *
605
     * @see test_create_courses_empty_field
606
     * @see test_update_courses_empty_field
607
     *
608
     * @return array
609
     */
11 efrain 610
    public static function course_empty_field_provider(): array {
1 efrain 611
        return [
612
            [[
613
                'fullname' => '',
614
                'shortname' => 'ws101',
615
            ], 'fullname'],
616
            [[
617
                'fullname' => ' ',
618
                'shortname' => 'ws101',
619
            ], 'fullname'],
620
            [[
621
                'fullname' => 'Web Services',
622
                'shortname' => '',
623
            ], 'shortname'],
624
            [[
625
                'fullname' => 'Web Services',
626
                'shortname' => ' ',
627
            ], 'shortname'],
628
        ];
629
    }
630
 
631
    /**
632
     * Test creating courses with empty fields throws an exception
633
     *
634
     * @param array $course
635
     * @param string $expectedemptyfield
636
     *
637
     * @dataProvider course_empty_field_provider
638
     */
639
    public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void {
640
        $this->resetAfterTest();
641
        $this->setAdminUser();
642
 
643
        // Create a category for the new course.
644
        $course['categoryid'] = $this->getDataGenerator()->create_category()->id;
645
 
646
        $this->expectException(moodle_exception::class);
647
        $this->expectExceptionMessageMatches("/{$expectedemptyfield}/");
648
        core_course_external::create_courses([$course]);
649
    }
650
 
651
    /**
652
     * Test updating courses with empty fields returns warnings
653
     *
654
     * @param array $course
655
     * @param string $expectedemptyfield
656
     *
657
     * @dataProvider course_empty_field_provider
658
     */
659
    public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void {
660
        $this->resetAfterTest();
661
        $this->setAdminUser();
662
 
663
        // Create a course to update.
664
        $course['id'] = $this->getDataGenerator()->create_course()->id;
665
 
666
        $result = core_course_external::update_courses([$course]);
667
        $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result);
668
 
669
        $this->assertCount(1, $result['warnings']);
670
 
671
        $warning = reset($result['warnings']);
672
        $this->assertEquals('errorinvalidparam', $warning['warningcode']);
673
        $this->assertStringContainsString($expectedemptyfield, $warning['message']);
674
    }
675
 
676
    /**
677
     * Test delete_courses
678
     */
11 efrain 679
    public function test_delete_courses(): void {
1 efrain 680
        global $DB, $USER;
681
 
682
        $this->resetAfterTest(true);
683
 
684
        // Admin can delete a course.
685
        $this->setAdminUser();
686
        // Validate_context() will fail as the email is not set by $this->setAdminUser().
687
        $USER->email = 'emailtopass@example.com';
688
 
689
        $course1  = self::getDataGenerator()->create_course();
690
        $course2  = self::getDataGenerator()->create_course();
691
        $course3  = self::getDataGenerator()->create_course();
692
 
693
        // Delete courses.
694
        $result = core_course_external::delete_courses(array($course1->id, $course2->id));
695
        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
696
        // Check for 0 warnings.
697
        $this->assertEquals(0, count($result['warnings']));
698
 
699
        // Check $course 1 and 2 are deleted.
700
        $notdeletedcount = $DB->count_records_select('course',
701
            'id IN ( ' . $course1->id . ',' . $course2->id . ')');
702
        $this->assertEquals(0, $notdeletedcount);
703
 
704
        // Try to delete non-existent course.
705
        $result = core_course_external::delete_courses(array($course1->id));
706
        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
707
        // Check for 1 warnings.
708
        $this->assertEquals(1, count($result['warnings']));
709
 
710
        // Try to delete Frontpage course.
711
        $result = core_course_external::delete_courses(array(0));
712
        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
713
        // Check for 1 warnings.
714
        $this->assertEquals(1, count($result['warnings']));
715
 
716
         // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
717
        $student1 = self::getDataGenerator()->create_user();
718
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
719
        $this->getDataGenerator()->enrol_user($student1->id,
720
                                              $course3->id,
721
                                              $studentrole->id);
722
        $this->setUser($student1);
723
        $result = core_course_external::delete_courses(array($course3->id));
724
        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
725
        // Check for 1 warnings.
726
        $this->assertEquals(1, count($result['warnings']));
727
 
728
         // Fail when the user is not allow to access the course (enrolled) or is not admin.
729
        $this->setGuestUser();
730
        $this->expectException('require_login_exception');
731
 
732
        $result = core_course_external::delete_courses(array($course3->id));
733
        $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
734
    }
735
 
736
    /**
737
     * Test get_courses
738
     */
11 efrain 739
    public function test_get_courses(): void {
1 efrain 740
        global $DB;
741
 
742
        $this->resetAfterTest(true);
743
 
744
        $generatedcourses = array();
745
        $coursedata['idnumber'] = 'idnumbercourse1';
746
        // Adding tags here to check that format_string is applied.
747
        $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
748
        $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
749
        $coursedata['summary'] = 'Course 1 description';
750
        $coursedata['summaryformat'] = FORMAT_MOODLE;
751
        $course1  = self::getDataGenerator()->create_course($coursedata);
752
 
753
        $fieldcategory = self::getDataGenerator()->create_custom_field_category(
754
            ['name' => 'Other fields']);
755
 
756
        $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
757
            'categoryid' => $fieldcategory->get('id')];
758
        $field = self::getDataGenerator()->create_custom_field($customfield);
759
 
760
        $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
761
 
762
        $generatedcourses[$course1->id] = $course1;
763
        $course2  = self::getDataGenerator()->create_course();
764
        $generatedcourses[$course2->id] = $course2;
765
        $course3  = self::getDataGenerator()->create_course(array('format' => 'topics'));
766
        $generatedcourses[$course3->id] = $course3;
767
        $course4  = self::getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]);
768
        $generatedcourses[$course4->id] = $course4;
769
 
770
        // Set the required capabilities by the external function.
771
        $context = context_system::instance();
772
        $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
773
        $this->assignUserCapability('moodle/course:update',
774
                context_course::instance($course1->id)->id, $roleid);
775
        $this->assignUserCapability('moodle/course:update',
776
                context_course::instance($course2->id)->id, $roleid);
777
        $this->assignUserCapability('moodle/course:update',
778
                context_course::instance($course3->id)->id, $roleid);
779
        $this->assignUserCapability('moodle/course:update',
780
                context_course::instance($course4->id)->id, $roleid);
781
 
782
        $courses = core_course_external::get_courses(array('ids' =>
783
            array($course1->id, $course2->id, $course4->id)));
784
 
785
        // We need to execute the return values cleaning process to simulate the web service server.
786
        $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
787
 
788
        // Check we retrieve the good total number of courses.
789
        $this->assertEquals(3, count($courses));
790
 
791
        foreach ($courses as $course) {
792
            $coursecontext = context_course::instance($course['id']);
793
            $dbcourse = $generatedcourses[$course['id']];
794
            $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
795
            $this->assertEquals(
796
                $course['fullname'],
797
                \core_external\util::format_string($dbcourse->fullname, $coursecontext->id)
798
            );
799
            $this->assertEquals(
800
                $course['displayname'],
801
                \core_external\util::format_string(get_course_display_name_for_list($dbcourse), $coursecontext->id)
802
            );
803
            // Summary was converted to the HTML format.
804
            $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
805
            $this->assertEquals($course['summaryformat'], FORMAT_HTML);
806
            $this->assertEquals($course['shortname'], \core_external\util::format_string($dbcourse->shortname, $coursecontext->id));
807
            $this->assertEquals($course['categoryid'], $dbcourse->category);
808
            $this->assertEquals($course['format'], $dbcourse->format);
809
            $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
810
            $this->assertEquals($course['newsitems'], $dbcourse->newsitems);
811
            $this->assertEquals($course['startdate'], $dbcourse->startdate);
812
            $this->assertEquals($course['enddate'], $dbcourse->enddate);
813
            $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number());
814
            $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes);
815
            $this->assertEquals($course['showreports'], $dbcourse->showreports);
816
            $this->assertEquals($course['visible'], $dbcourse->visible);
817
            $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections);
818
            $this->assertEquals($course['groupmode'], $dbcourse->groupmode);
819
            $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce);
820
            $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid);
821
            $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify);
822
            $this->assertEquals($course['lang'], $dbcourse->lang);
823
            $this->assertEquals($course['forcetheme'], $dbcourse->theme);
824
            $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion);
825
            if ($dbcourse->format === 'topics') {
826
                $this->assertEquals($course['courseformatoptions'], array(
827
                    array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections),
828
                    array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
829
                ));
830
            }
831
 
832
            // Assert custom field that we previously added to test course 4.
833
            if ($dbcourse->id == $course4->id) {
834
                $this->assertEquals([
835
                    'shortname' => $customfield['shortname'],
836
                    'name' => $customfield['name'],
837
                    'type' => $customfield['type'],
838
                    'value' => $customfieldvalue['value'],
839
                    'valueraw' => $customfieldvalue['value'],
840
                ], $course['customfields'][0]);
841
            }
842
        }
843
 
844
        // Get all courses in the DB
845
        $courses = core_course_external::get_courses(array());
846
 
847
        // We need to execute the return values cleaning process to simulate the web service server.
848
        $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
849
 
850
        $this->assertEquals($DB->count_records('course'), count($courses));
851
    }
852
 
853
    /**
854
     * Test retrieving courses returns custom field data
855
     */
856
    public function test_get_courses_customfields(): void {
857
        $this->resetAfterTest();
858
        $this->setAdminUser();
859
 
860
        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
861
        $datefield = $this->getDataGenerator()->create_custom_field([
862
            'categoryid' => $fieldcategory->get('id'),
863
            'shortname' => 'mydate',
864
            'name' => 'My date',
865
            'type' => 'date',
866
        ]);
867
 
868
        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
869
            [
870
                'shortname' => $datefield->get('shortname'),
871
                'value' => 1580389200, // 30/01/2020 13:00 GMT.
872
            ],
873
        ]]);
874
 
875
        $courses = external_api::clean_returnvalue(
876
            core_course_external::get_courses_returns(),
877
            core_course_external::get_courses(['ids' => [$newcourse->id]])
878
        );
879
 
880
        $this->assertCount(1, $courses);
881
        $course = reset($courses);
882
 
883
        $this->assertArrayHasKey('customfields', $course);
884
        $this->assertCount(1, $course['customfields']);
885
 
886
        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
887
        $this->assertEquals([
888
            'name' => $datefield->get('name'),
889
            'shortname' => $datefield->get('shortname'),
890
            'type' => $datefield->get('type'),
891
            'value' => userdate(1580389200),
892
            'valueraw' => 1580389200,
893
        ], reset($course['customfields']));
894
    }
895
 
896
    /**
897
     * Test get_courses without capability
898
     */
11 efrain 899
    public function test_get_courses_without_capability(): void {
1 efrain 900
        $this->resetAfterTest(true);
901
 
902
        $course1 = $this->getDataGenerator()->create_course();
903
        $this->setUser($this->getDataGenerator()->create_user());
904
 
905
        // No permissions are required to get the site course.
906
        $courses = core_course_external::get_courses(array('ids' => [SITEID]));
907
        $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
908
 
909
        $this->assertEquals(1, count($courses));
910
        $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
911
        $this->assertEquals('site', $courses[0]['format']);
912
 
913
        // Requesting course without being enrolled or capability to view it will throw an exception.
914
        try {
915
            core_course_external::get_courses(array('ids' => [$course1->id]));
916
            $this->fail('Exception expected');
917
        } catch (moodle_exception $e) {
918
            $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
919
        }
920
    }
921
 
922
    /**
923
     * Test search_courses
924
     */
11 efrain 925
    public function test_search_courses(): void {
1 efrain 926
 
927
        global $DB;
928
 
929
        $this->resetAfterTest(true);
930
        $this->setAdminUser();
931
        $generatedcourses = array();
932
        $coursedata1['fullname'] = 'FIRST COURSE';
933
        $course1  = self::getDataGenerator()->create_course($coursedata1);
934
 
935
        $page = new moodle_page();
936
        $page->set_course($course1);
937
        $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
938
 
939
        $coursedata2['fullname'] = 'SECOND COURSE';
940
        $course2  = self::getDataGenerator()->create_course($coursedata2);
941
 
942
        $page = new moodle_page();
943
        $page->set_course($course2);
944
        $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
945
 
946
        // Search by name.
947
        $results = core_course_external::search_courses('search', 'FIRST');
948
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
949
        $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
950
        $this->assertCount(1, $results['courses']);
951
 
952
        // Create the forum.
953
        $record = new stdClass();
954
        $record->introformat = FORMAT_HTML;
955
        $record->course = $course2->id;
956
        // Set Aggregate type = Average of ratings.
957
        $forum = self::getDataGenerator()->create_module('forum', $record);
958
 
959
        // Search by module.
960
        $results = core_course_external::search_courses('modulelist', 'forum');
961
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
962
        $this->assertEquals(1, $results['total']);
963
 
964
        // Enable coursetag option.
965
        set_config('block_tags_showcoursetags', true);
966
        // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
967
        core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
968
                array('TAG-LABEL ON SECOND COURSE'));
969
        $taginstance = $DB->get_record('tag_instance',
970
                array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
971
 
972
        // Search by tagid.
973
        $results = core_course_external::search_courses('tagid', $taginstance->tagid);
974
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
975
        $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
976
 
977
        // Search by block (use news_items default block).
978
        $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
979
        $results = core_course_external::search_courses('blocklist', $blockid);
980
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
981
        $this->assertEquals(2, $results['total']);
982
 
983
        // Now as a normal user.
984
        $user = self::getDataGenerator()->create_user();
985
 
986
        // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
987
        $coursedata3['fullname'] = 'HIDDEN COURSE';
988
        $coursedata3['visible'] = 0;
989
        $course3  = self::getDataGenerator()->create_course($coursedata3);
990
        $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
991
 
992
        $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
993
        $this->setUser($user);
994
 
995
        $results = core_course_external::search_courses('search', 'FIRST');
996
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
997
        $this->assertCount(1, $results['courses']);
998
        $this->assertEquals(1, $results['total']);
999
        $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
1000
 
1001
        // Check that we can see all courses without the limit to enrolled setting.
1002
        $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
1003
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1004
        $this->assertCount(2, $results['courses']);
1005
        $this->assertEquals(2, $results['total']);
1006
 
1007
        // Check that we only see our enrolled course when limiting.
1008
        $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
1009
        $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
1010
        $this->assertCount(1, $results['courses']);
1011
        $this->assertEquals(1, $results['total']);
1012
        $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
1013
 
1014
        // Search by block (use news_items default block). Should fail (only admins allowed).
1015
        $this->expectException('required_capability_exception');
1016
        $results = core_course_external::search_courses('blocklist', $blockid);
1017
    }
1018
 
1019
    /**
1020
     * Test searching for courses returns custom field data
1021
     */
1022
    public function test_search_courses_customfields(): void {
1023
        $this->resetAfterTest();
1024
        $this->setAdminUser();
1025
 
1026
        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
1027
        $datefield = $this->getDataGenerator()->create_custom_field([
1028
            'categoryid' => $fieldcategory->get('id'),
1029
            'shortname' => 'mydate',
1030
            'name' => 'My date',
1031
            'type' => 'date',
1032
        ]);
1033
 
1034
        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
1035
            [
1036
                'shortname' => $datefield->get('shortname'),
1037
                'value' => 1580389200, // 30/01/2020 13:00 GMT.
1038
            ],
1039
        ]]);
1040
 
1041
        $result = external_api::clean_returnvalue(
1042
            core_course_external::search_courses_returns(),
1043
            core_course_external::search_courses('search', $newcourse->shortname)
1044
        );
1045
 
1046
        $this->assertCount(1, $result['courses']);
1047
        $course = reset($result['courses']);
1048
 
1049
        $this->assertArrayHasKey('customfields', $course);
1050
        $this->assertCount(1, $course['customfields']);
1051
 
1052
        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
1053
        $this->assertEquals([
1054
            'name' => $datefield->get('name'),
1055
            'shortname' => $datefield->get('shortname'),
1056
            'type' => $datefield->get('type'),
1057
            'value' => userdate(1580389200),
1058
            'valueraw' => 1580389200,
1059
        ], reset($course['customfields']));
1060
    }
1061
 
1062
    /**
1063
     * Create a course with contents
1064
     * @return array A list with the course object and course modules objects
1065
     */
1066
    private function prepare_get_course_contents_test() {
1067
        global $DB, $CFG;
1068
 
1069
        $CFG->allowstealth = 1; // Allow stealth activities.
1070
        $CFG->enablecompletion = true;
1071
        // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections.
1072
        $course  = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]);
1073
 
1074
        $forumdescription = 'This is the forum description';
1075
        $forum = $this->getDataGenerator()->create_module('forum',
1076
            array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
1077
            array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
1078
        $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1079
        // Add discussions to the tracking forced forum.
1080
        $record = new stdClass();
1081
        $record->course = $course->id;
1082
        $record->userid = 0;
1083
        $record->forum = $forum->id;
1084
        $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
1085
        $data = $this->getDataGenerator()->create_module('data',
1086
            array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
1087
        $datacm = get_coursemodule_from_instance('data', $data->id);
1088
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1089
        $pagecm = get_coursemodule_from_instance('page', $page->id);
1090
        // This is an stealth page (set by visibleoncoursepage).
1091
        $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
1092
        $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
1093
                So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
1094
        $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
1095
            'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL));
1096
        $labelcm = get_coursemodule_from_instance('label', $label->id);
1097
        $tomorrow = time() + DAYSECS;
1098
        // Module with availability restrictions not met.
1099
        $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},'
1100
                .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}';
1101
        $url = $this->getDataGenerator()->create_module('url',
1102
            array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
1103
                'popupwidth' => 100, 'popupheight' => 100),
1104
            array('availability' => $availability));
1105
        $urlcm = get_coursemodule_from_instance('url', $url->id);
1106
        // Module for the last section.
1107
        $this->getDataGenerator()->create_module('url',
1108
            array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
1109
        // Module for section 1 with availability restrictions met.
1110
        $yesterday = time() - DAYSECS;
1111
        $this->getDataGenerator()->create_module('url',
1112
            array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
1113
            array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
1114
 
1115
        // Set the required capabilities by the external function.
1116
        $context = context_course::instance($course->id);
1117
        $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
1118
        $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
1119
        $this->assignUserCapability('mod/data:view', $context->id, $roleid);
1120
 
1121
        $conditions = array('course' => $course->id, 'section' => 2);
1122
        $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
1123
 
1124
        // Add date availability condition not met for section 3.
1125
        $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
1126
        $DB->set_field('course_sections', 'availability', $availability,
1127
                array('course' => $course->id, 'section' => 3));
1128
 
1129
        // Create resource for last section.
1130
        $pageinhiddensection = $this->getDataGenerator()->create_module('page',
1131
            array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
1132
        // Set not visible last section.
1133
        $DB->set_field('course_sections', 'visible', 0,
1134
                array('course' => $course->id, 'section' => 4));
1135
 
1136
        $forumcompleteauto = $this->getDataGenerator()->create_module('forum',
1137
            array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
1138
            array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
1139
        $forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
1140
        $sectionrecord = $DB->get_record('course_sections', $conditions);
1141
        // Invalidate the section cache by given section number.
1142
        course_modinfo::purge_course_section_cache_by_number($sectionrecord->course, $sectionrecord->section);
1143
        rebuild_course_cache($course->id, true, true);
1144
 
1145
        return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
1146
    }
1147
 
1148
    /**
1149
     * Test get_course_contents
1150
     */
11 efrain 1151
    public function test_get_course_contents(): void {
1 efrain 1152
        global $CFG;
1153
        $this->resetAfterTest(true);
1154
 
1155
        $CFG->forum_allowforcedreadtracking = 1;
1156
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1157
 
1158
        // Create a resource with all the appearance options enabled. By default it's a text file and will be added to section 1.
1159
        $record = (object) [
1160
            'course' => $course->id,
1161
            'showsize' => 1,
1162
            'showtype' => 1,
1163
            'showdate' => 1,
1164
        ];
1165
        $resource = self::getDataGenerator()->create_module('resource', $record);
1166
        $h5pactivity = self::getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
1167
 
1168
        // We first run the test as admin.
1169
        $this->setAdminUser();
1170
        $sections = core_course_external::get_course_contents($course->id, array());
1171
        // We need to execute the return values cleaning process to simulate the web service server.
1172
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1173
 
1174
        $modinfo = get_fast_modinfo($course);
1175
        $testexecuted = 0;
1176
        foreach ($sections[0]['modules'] as $module) {
1177
            if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
1178
                $cm = $modinfo->cms[$forumcm->id];
1179
                $formattedtext = format_text($cm->content, FORMAT_HTML,
1180
                    array('noclean' => true, 'para' => false, 'filter' => false));
1181
                $this->assertEquals($formattedtext, $module['description']);
1182
                $this->assertEquals($forumcm->instance, $module['instance']);
1183
                $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
1184
                $this->assertFalse($module['noviewlink']);
1185
                $this->assertNotEmpty($module['description']);  // Module showdescription is on.
1186
                // Afterlink for forums has been removed; it has been moved to the new activity badge content.
1187
                $this->assertEmpty($module['afterlink']);
1188
                $this->assertEquals('1 unread post', $module['activitybadge']['badgecontent']);
1189
                $this->assertEquals('bg-dark text-white', $module['activitybadge']['badgestyle']);
1190
                $this->assertEquals(
1191
                    plugin_supports(
1192
                        'mod',
1193
                        'forum',
1194
                        FEATURE_MOD_PURPOSE,
1195
                        MOD_PURPOSE_OTHER
1196
                    ), $module['purpose']
1197
                );
1198
                $this->assertFalse($module['branded']);
1199
                $this->assertStringContainsString('trackingtype', $module['customdata']);   // The customdata is JSON encoded.
1200
                $testexecuted = $testexecuted + 2;
1201
            } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
1202
                $cm = $modinfo->cms[$labelcm->id];
1203
                $formattedtext = format_text($cm->content, FORMAT_HTML,
1204
                    array('noclean' => true, 'para' => false, 'filter' => false));
1205
                $this->assertEquals($formattedtext, $module['description']);
1206
                $this->assertEquals($labelcm->instance, $module['instance']);
1207
                $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
1208
                $this->assertTrue($module['noviewlink']);
1209
                $this->assertNotEmpty($module['description']);  // Label always prints the description.
1210
                $this->assertEquals(
1211
                    plugin_supports(
1212
                        'mod',
1213
                        'label',
1214
                        FEATURE_MOD_PURPOSE,
1215
                        MOD_PURPOSE_OTHER
1216
                    ), $module['purpose']
1217
                );
1218
                $this->assertFalse($module['branded']);
1219
                $testexecuted = $testexecuted + 1;
1220
            } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
1221
                $this->assertStringContainsString('customcompletionrules', $module['customdata']);
1222
                $this->assertFalse($module['noviewlink']);
1223
                $this->assertArrayNotHasKey('description', $module);
1224
                $this->assertEquals(
1225
                    plugin_supports(
1226
                        'mod',
1227
                        'data',
1228
                        FEATURE_MOD_PURPOSE,
1229
                        MOD_PURPOSE_OTHER
1230
                    ), $module['purpose']
1231
                );
1232
                $this->assertFalse($module['branded']);
1233
                $testexecuted = $testexecuted + 1;
1234
            } else if ($module['instance'] == $resource->id && $module['modname'] == 'resource') {
1235
                // Resources have both, afterlink for the size and the update date and activitybadge for the file type.
1236
                $this->assertStringContainsString('32 bytes', $module['afterlink']);
1237
                $this->assertEquals('TXT', $module['activitybadge']['badgecontent']);
1238
                $this->assertEquals('badge-none', $module['activitybadge']['badgestyle']);
1239
                $this->assertEquals(
1240
                    plugin_supports(
1241
                        'mod',
1242
                        'resource',
1243
                        FEATURE_MOD_PURPOSE,
1244
                        MOD_PURPOSE_OTHER
1245
                    ), $module['purpose']
1246
                );
1247
                $this->assertFalse($module['branded']);
1248
                $testexecuted = $testexecuted + 1;
1249
            } else if ($module['instance'] == $h5pactivity->id && $module['modname'] == 'h5pactivity') {
1250
                $this->assertEquals(
1251
                    plugin_supports(
1252
                        'mod',
1253
                        'h5pactivity',
1254
                        FEATURE_MOD_PURPOSE,
1255
                        MOD_PURPOSE_OTHER
1256
                    ), $module['purpose']
1257
                );
1258
                $this->assertTrue($module['branded']);
1259
                $testexecuted = $testexecuted + 1;
1260
            }
1261
        }
1262
        foreach ($sections[2]['modules'] as $module) {
1263
            if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
1264
                $this->assertStringContainsString('width=100,height=100', $module['onclick']);
1265
                $testexecuted = $testexecuted + 1;
1266
            }
1267
        }
1268
 
1269
        $CFG->forum_allowforcedreadtracking = 0;    // Recover original value.
1270
        forum_tp_count_forum_unread_posts($forumcm, $course, true);    // Reset static cache for further tests.
1271
 
1272
        $this->assertEquals(7, $testexecuted);
1273
        $this->assertEquals(0, $sections[0]['section']);
1274
 
1275
        $this->assertCount(8, $sections[0]['modules']);
1276
        $this->assertCount(1, $sections[1]['modules']);
1277
        $this->assertCount(1, $sections[2]['modules']);
1278
        $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
1279
        $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
1280
        $this->assertNotEmpty($sections[3]['availabilityinfo']);
1281
        $this->assertEquals(1, $sections[1]['section']);
1282
        $this->assertEquals(2, $sections[2]['section']);
1283
        $this->assertEquals(3, $sections[3]['section']);
1284
        $this->assertEquals(4, $sections[4]['section']);
1285
        $this->assertStringContainsString('<iframe', $sections[2]['summary']);
1286
        $this->assertStringContainsString('</iframe>', $sections[2]['summary']);
1287
        $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
1288
        try {
1289
            $sections = core_course_external::get_course_contents($course->id,
1290
                                                                    array(array("name" => "invalid", "value" => 1)));
1291
            $this->fail('Exception expected due to invalid option.');
1292
        } catch (moodle_exception $e) {
1293
            $this->assertEquals('errorinvalidparam', $e->errorcode);
1294
        }
1295
    }
1296
 
1297
 
1298
    /**
1299
     * Test get_course_contents as student
1300
     */
11 efrain 1301
    public function test_get_course_contents_student(): void {
1 efrain 1302
        global $DB;
1303
        $this->resetAfterTest(true);
1304
 
1305
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1306
 
1307
        $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1308
        $user = self::getDataGenerator()->create_user();
1309
        self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1310
        $this->setUser($user);
1311
 
1312
        $sections = core_course_external::get_course_contents($course->id, array());
1313
        // We need to execute the return values cleaning process to simulate the web service server.
1314
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1315
 
1316
        $this->assertCount(4, $sections); // Nothing for the not visible section.
1317
        $this->assertCount(6, $sections[0]['modules']);
1318
        $this->assertCount(1, $sections[1]['modules']);
1319
        $this->assertCount(1, $sections[2]['modules']);
1320
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1321
 
1322
        $this->assertNotEmpty($sections[3]['availabilityinfo']);
1323
        $this->assertEquals(1, $sections[1]['section']);
1324
        $this->assertEquals(2, $sections[2]['section']);
1325
        $this->assertEquals(3, $sections[3]['section']);
1326
        // The module with the availability restriction met is returning contents.
1327
        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1328
        // The module with the availability restriction not met is not returning contents.
1329
        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1330
 
1331
        // Now include flag for returning stealth information (fake section).
1332
        $sections = core_course_external::get_course_contents($course->id,
1333
            array(array("name" => "includestealthmodules", "value" => 1)));
1334
        // We need to execute the return values cleaning process to simulate the web service server.
1335
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1336
 
1337
        $this->assertCount(5, $sections); // Include fake section with stealth activities.
1338
        $this->assertCount(6, $sections[0]['modules']);
1339
        $this->assertCount(1, $sections[1]['modules']);
1340
        $this->assertCount(1, $sections[2]['modules']);
1341
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1342
        $this->assertCount(1, $sections[4]['modules']); // One stealth module.
1343
        $this->assertEquals(-1, $sections[4]['id']);
1344
    }
1345
 
1346
    /**
1347
     * Test get_course_contents excluding modules
1348
     */
11 efrain 1349
    public function test_get_course_contents_excluding_modules(): void {
1 efrain 1350
        $this->resetAfterTest(true);
1351
 
1352
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1353
 
1354
        // Test exclude modules.
1355
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
1356
 
1357
        // We need to execute the return values cleaning process to simulate the web service server.
1358
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1359
 
1360
        $this->assertEmpty($sections[0]['modules']);
1361
        $this->assertEmpty($sections[1]['modules']);
1362
    }
1363
 
1364
    /**
1365
     * Test get_course_contents excluding contents
1366
     */
11 efrain 1367
    public function test_get_course_contents_excluding_contents(): void {
1 efrain 1368
        $this->resetAfterTest(true);
1369
 
1370
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1371
 
1372
        // Test exclude modules.
1373
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
1374
 
1375
        // We need to execute the return values cleaning process to simulate the web service server.
1376
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1377
 
1378
        foreach ($sections as $section) {
1379
            foreach ($section['modules'] as $module) {
1380
                // Only resources return contents.
1381
                if (isset($module['contents'])) {
1382
                    $this->assertEmpty($module['contents']);
1383
                }
1384
            }
1385
        }
1386
    }
1387
 
1388
    /**
1389
     * Test get_course_contents filtering by section number
1390
     */
11 efrain 1391
    public function test_get_course_contents_section_number(): void {
1 efrain 1392
        $this->resetAfterTest(true);
1393
 
1394
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1395
 
1396
        // Test exclude modules.
1397
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
1398
 
1399
        // We need to execute the return values cleaning process to simulate the web service server.
1400
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1401
 
1402
        $this->assertCount(1, $sections);
1403
        $this->assertCount(6, $sections[0]['modules']);
1404
    }
1405
 
1406
    /**
1407
     * Test get_course_contents filtering by cmid
1408
     */
11 efrain 1409
    public function test_get_course_contents_cmid(): void {
1 efrain 1410
        $this->resetAfterTest(true);
1411
 
1412
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1413
 
1414
        // Test exclude modules.
1415
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
1416
 
1417
        // We need to execute the return values cleaning process to simulate the web service server.
1418
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1419
 
1420
        $this->assertCount(4, $sections);
1421
        $this->assertCount(1, $sections[0]['modules']);
1422
        $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1423
    }
1424
 
1425
 
1426
    /**
1427
     * Test get_course_contents filtering by cmid and section
1428
     */
11 efrain 1429
    public function test_get_course_contents_section_cmid(): void {
1 efrain 1430
        $this->resetAfterTest(true);
1431
 
1432
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1433
 
1434
        // Test exclude modules.
1435
        $sections = core_course_external::get_course_contents($course->id, array(
1436
                                                                        array("name" => "cmid", "value" => $forumcm->id),
1437
                                                                        array("name" => "sectionnumber", "value" => 0)
1438
                                                                        ));
1439
 
1440
        // We need to execute the return values cleaning process to simulate the web service server.
1441
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1442
 
1443
        $this->assertCount(1, $sections);
1444
        $this->assertCount(1, $sections[0]['modules']);
1445
        $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1446
    }
1447
 
1448
    /**
1449
     * Test get_course_contents filtering by modname
1450
     */
11 efrain 1451
    public function test_get_course_contents_modname(): void {
1 efrain 1452
        $this->resetAfterTest(true);
1453
 
1454
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1455
 
1456
        // Test exclude modules.
1457
        $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
1458
 
1459
        // We need to execute the return values cleaning process to simulate the web service server.
1460
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1461
 
1462
        $this->assertCount(4, $sections);
1463
        $this->assertCount(2, $sections[0]['modules']);
1464
        $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
1465
    }
1466
 
1467
    /**
1468
     * Test get_course_contents filtering by modname
1469
     */
11 efrain 1470
    public function test_get_course_contents_modid(): void {
1 efrain 1471
        $this->resetAfterTest(true);
1472
 
1473
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1474
 
1475
        // Test exclude modules.
1476
        $sections = core_course_external::get_course_contents($course->id, array(
1477
                                                                            array("name" => "modname", "value" => "page"),
1478
                                                                            array("name" => "modid", "value" => $pagecm->instance),
1479
                                                                            ));
1480
 
1481
        // We need to execute the return values cleaning process to simulate the web service server.
1482
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1483
 
1484
        $this->assertCount(4, $sections);
1485
        $this->assertCount(1, $sections[0]['modules']);
1486
        $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
1487
        $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
1488
    }
1489
 
1490
    /**
1491
     * Test get_course_contents returns downloadcontent value.
1492
     */
11 efrain 1493
    public function test_get_course_contents_downloadcontent(): void {
1 efrain 1494
        $this->resetAfterTest();
1495
 
1496
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1497
 
1498
        // Test exclude modules.
1499
        $sections = core_course_external::get_course_contents($course->id, [
1500
            ['name' => 'modname', 'value' => 'page'],
1501
            ['name' => 'modid', 'value' => $pagecm->instance]
1502
        ]);
1503
 
1504
        // We need to execute the return values cleaning process to simulate the web service server.
1505
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1506
        $this->assertCount(1, $sections[0]['modules']);
1507
        $this->assertEquals('page', $sections[0]['modules'][0]['modname']);
1508
        $this->assertEquals($pagecm->downloadcontent, $sections[0]['modules'][0]['downloadcontent']);
1509
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $sections[0]['modules'][0]['downloadcontent']);
1510
    }
1511
 
1512
    /**
1513
     * Test get course contents completion manual
1514
     */
11 efrain 1515
    public function test_get_course_contents_completion_manual(): void {
1 efrain 1516
        global $CFG;
1517
        $this->resetAfterTest(true);
1518
 
1519
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1520
            $this->prepare_get_course_contents_test();
1521
        availability_completion\condition::wipe_static_cache();
1522
 
1523
        // Test activity not completed yet.
1524
        $result = core_course_external::get_course_contents($course->id, array(
1525
            array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1526
        // We need to execute the return values cleaning process to simulate the web service server.
1527
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1528
 
1529
        $completiondata = $result[0]['modules'][0]["completiondata"];
1530
        $this->assertCount(1, $result[0]['modules']);
1531
        $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
1532
        $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1533
        $this->assertEquals(0, $completiondata['state']);
1534
        $this->assertEquals(0, $completiondata['timecompleted']);
1535
        $this->assertEmpty($completiondata['overrideby']);
1536
        $this->assertFalse($completiondata['valueused']);
1537
        $this->assertTrue($completiondata['hascompletion']);
1538
        $this->assertFalse($completiondata['isautomatic']);
1539
        $this->assertFalse($completiondata['istrackeduser']);
1540
        $this->assertTrue($completiondata['uservisible']);
1541
        $this->assertFalse($completiondata['isoverallcomplete']);
1542
 
1543
        // Set activity completed.
1544
        core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
1545
 
1546
        $result = core_course_external::get_course_contents($course->id, array(
1547
            array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1548
        // We need to execute the return values cleaning process to simulate the web service server.
1549
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1550
 
1551
        $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
1552
        $this->assertTrue($result[0]['modules'][0]["completiondata"]['isoverallcomplete']);
1553
        $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
1554
        $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
1555
 
1556
        // Test activity with completion value that is used in an availability condition.
1557
        $result = core_course_external::get_course_contents($course->id, array(
1558
                array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance)));
1559
        // We need to execute the return values cleaning process to simulate the web service server.
1560
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1561
 
1562
        $completiondata = $result[0]['modules'][0]["completiondata"];
1563
        $this->assertCount(1, $result[0]['modules']);
1564
        $this->assertEquals("label", $result[0]['modules'][0]["modname"]);
1565
        $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
1566
        $this->assertEquals(0, $completiondata['state']);
1567
        $this->assertEquals(0, $completiondata['timecompleted']);
1568
        $this->assertEmpty($completiondata['overrideby']);
1569
        $this->assertTrue($completiondata['valueused']);
1570
        $this->assertTrue($completiondata['hascompletion']);
1571
        $this->assertFalse($completiondata['isautomatic']);
1572
        $this->assertFalse($completiondata['istrackeduser']);
1573
        $this->assertTrue($completiondata['uservisible']);
1574
        $this->assertFalse($completiondata['isoverallcomplete']);
1575
 
1576
        // Disable completion.
1577
        $CFG->enablecompletion = 0;
1578
        $result = core_course_external::get_course_contents($course->id, array(
1579
            array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
1580
        // We need to execute the return values cleaning process to simulate the web service server.
1581
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1582
 
1583
        $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
1584
    }
1585
 
1586
    /**
1587
     * Test get course contents completion auto
1588
     */
11 efrain 1589
    public function test_get_course_contents_completion_auto(): void {
1 efrain 1590
        global $CFG;
1591
        $this->resetAfterTest(true);
1592
 
1593
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
1594
            $this->prepare_get_course_contents_test();
1595
        availability_completion\condition::wipe_static_cache();
1596
 
1597
        // Test activity not completed yet.
1598
        $result = core_course_external::get_course_contents($course->id, [
1599
            [
1600
                "name" => "modname",
1601
                "value" => "forum"
1602
            ],
1603
            [
1604
                "name" => "modid",
1605
                "value" => $forumcompleteautocm->instance
1606
            ]
1607
        ]);
1608
        // We need to execute the return values cleaning process to simulate the web service server.
1609
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1610
 
1611
        $forummod = $result[0]['modules'][0];
1612
        $completiondata = $forummod["completiondata"];
1613
        $this->assertCount(1, $result[0]['modules']);
1614
        $this->assertEquals("forum", $forummod["modname"]);
1615
        $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]);
1616
        $this->assertEquals(0, $completiondata['state']);
1617
        $this->assertEquals(0, $completiondata['timecompleted']);
1618
        $this->assertEmpty($completiondata['overrideby']);
1619
        $this->assertFalse($completiondata['valueused']);
1620
        $this->assertTrue($completiondata['hascompletion']);
1621
        $this->assertTrue($completiondata['isautomatic']);
1622
        $this->assertFalse($completiondata['istrackeduser']);
1623
        $this->assertTrue($completiondata['uservisible']);
1624
        $this->assertCount(1, $completiondata['details']);
1625
        $this->assertFalse($completiondata['isoverallcomplete']);
1626
    }
1627
 
1628
    /**
1629
     * Test mimetype is returned for resources with showtype set.
1630
     */
11 efrain 1631
    public function test_get_course_contents_including_mimetype(): void {
1 efrain 1632
        $this->resetAfterTest(true);
1633
 
1634
        $this->setAdminUser();
1635
        $course = self::getDataGenerator()->create_course();
1636
 
1637
        $record = new stdClass();
1638
        $record->course = $course->id;
1639
        $record->showtype = 1;
1640
        $resource = self::getDataGenerator()->create_module('resource', $record);
1641
 
1642
        $result = core_course_external::get_course_contents($course->id);
1643
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1644
        $this->assertCount(1, $result[0]['modules']);   // One module, first section.
1645
        $customdata = json_decode($result[0]['modules'][0]['customdata']);
1646
        $displayoptions = unserialize($customdata->displayoptions);
1647
        $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']);
1648
    }
1649
 
1650
    /**
1651
     * Test contents info is returned.
1652
     */
11 efrain 1653
    public function test_get_course_contents_contentsinfo(): void {
1 efrain 1654
        global $USER;
1655
 
1656
        $this->resetAfterTest(true);
1657
        $this->setAdminUser();
1658
        $timenow = time();
1659
 
1660
        $course = self::getDataGenerator()->create_course();
1661
 
1662
        $record = new stdClass();
1663
        $record->course = $course->id;
1664
        // One resource with one file.
1665
        $resource1 = self::getDataGenerator()->create_module('resource', $record);
1666
 
1667
        // More type of files.
1668
        $record->files = file_get_unused_draft_itemid();
1669
        $usercontext = context_user::instance($USER->id);
1670
        $extensions = array('txt', 'png', 'pdf');
1671
        $fs = get_file_storage();
1672
        foreach ($extensions as $key => $extension) {
1673
            // Add actual file there.
1674
            $filerecord = array('component' => 'user', 'filearea' => 'draft',
1675
                    'contextid' => $usercontext->id, 'itemid' => $record->files,
1676
                    'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
1677
            $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
1678
        }
1679
 
1680
        // Create file reference.
1681
        $repos = repository::get_instances(array('type' => 'user'));
1682
        $userrepository = reset($repos);
1683
 
1684
        // Create a user private file.
1685
        $userfilerecord = new stdClass;
1686
        $userfilerecord->contextid = $usercontext->id;
1687
        $userfilerecord->component = 'user';
1688
        $userfilerecord->filearea  = 'private';
1689
        $userfilerecord->itemid    = 0;
1690
        $userfilerecord->filepath  = '/';
1691
        $userfilerecord->filename  = 'userfile.txt';
1692
        $userfilerecord->source    = 'test';
1693
        $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
1694
        $userfileref = $fs->pack_reference($userfilerecord);
1695
 
1696
        // Clone latest "normal" file.
1697
        $filerefrecord = clone (object) $filerecord;
1698
        $filerefrecord->filename = 'testref.txt';
1699
        $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
1700
        // Set main file pointing to the file reference.
1701
        file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath,
1702
            $filerefrecord->filename, 1);
1703
 
1704
        // Once the reference has been created, create the file resource.
1705
        $resource2 = self::getDataGenerator()->create_module('resource', $record);
1706
 
1707
        $result = core_course_external::get_course_contents($course->id);
1708
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1709
        $this->assertCount(2, $result[0]['modules']);
1710
        foreach ($result[0]['modules'] as $module) {
1711
            if ($module['instance'] == $resource1->id) {
1712
                $this->assertEquals(1, $module['contentsinfo']['filescount']);
1713
                $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1714
                $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
1715
                $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
1716
            } else {
1717
                $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']);
1718
                $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
1719
                    $module['contents'][2]['filesize'] + $module['contents'][3]['filesize'];
1720
                $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
1721
                $this->assertEquals('user', $module['contentsinfo']['repositorytype']);
1722
                $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
1723
                $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
1724
            }
1725
        }
1726
    }
1727
 
1728
    /**
1729
     * Test get_course_contents when hidden sections are displayed.
1730
     */
11 efrain 1731
    public function test_get_course_contents_hiddensections(): void {
1 efrain 1732
        global $DB;
1733
        $this->resetAfterTest(true);
1734
 
1735
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1736
        // Force returning hidden sections.
1737
        $course->hiddensections = 0;
1738
        update_course($course);
1739
 
1740
        $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
1741
        $user = self::getDataGenerator()->create_user();
1742
        self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
1743
        $this->setUser($user);
1744
 
1745
        $sections = core_course_external::get_course_contents($course->id, array());
1746
        // We need to execute the return values cleaning process to simulate the web service server.
1747
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1748
 
1749
        $this->assertCount(5, $sections); // All the sections, including the "not visible" one.
1750
        $this->assertCount(6, $sections[0]['modules']);
1751
        $this->assertCount(1, $sections[1]['modules']);
1752
        $this->assertCount(1, $sections[2]['modules']);
1753
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1754
        $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1755
 
1756
        $this->assertNotEmpty($sections[3]['availabilityinfo']);
1757
        $this->assertEquals(1, $sections[1]['section']);
1758
        $this->assertEquals(2, $sections[2]['section']);
1759
        $this->assertEquals(3, $sections[3]['section']);
1760
        // The module with the availability restriction met is returning contents.
1761
        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
1762
        // The module with the availability restriction not met is not returning contents.
1763
        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
1764
 
1765
        // Now include flag for returning stealth information (fake section).
1766
        $sections = core_course_external::get_course_contents($course->id,
1767
            array(array("name" => "includestealthmodules", "value" => 1)));
1768
        // We need to execute the return values cleaning process to simulate the web service server.
1769
        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
1770
 
1771
        $this->assertCount(6, $sections); // Include fake section with stealth activities.
1772
        $this->assertCount(6, $sections[0]['modules']);
1773
        $this->assertCount(1, $sections[1]['modules']);
1774
        $this->assertCount(1, $sections[2]['modules']);
1775
        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
1776
        $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
1777
        $this->assertCount(1, $sections[5]['modules']); // One stealth module.
1778
        $this->assertEquals(-1, $sections[5]['id']);
1779
    }
1780
 
1781
    /**
1782
     * Test get course contents dates.
1783
     */
11 efrain 1784
    public function test_get_course_contents_dates(): void {
1 efrain 1785
        $this->resetAfterTest(true);
1786
 
1787
        $this->setAdminUser();
1788
        set_config('enablecourserelativedates', 1);
1789
 
1790
        // Course with just main section.
1791
        $timenow = time();
1792
        $course = self::getDataGenerator()->create_course(
1793
            ['numsections' => 0, 'relativedatesmode' => true, 'startdate' => $timenow - DAYSECS]);
1794
 
1795
        $teacher = self::getDataGenerator()->create_user();
1796
        self::getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
1797
 
1798
        $this->setUser($teacher);
1799
 
1800
        // Create resource (empty dates).
1801
        $resource = self::getDataGenerator()->create_module('resource', ['course' => $course->id]);
1802
        // Create activities with dates.
1803
        $resource = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'duedate' => $timenow]);
1804
        $resource = self::getDataGenerator()->create_module('choice',
1805
            ['course' => $course->id, 'timeopen' => $timenow, 'timeclose' => $timenow + DAYSECS]);
1806
        $resource = self::getDataGenerator()->create_module('assign',
1807
            ['course' => $course->id, 'allowsubmissionsfromdate' => $timenow]);
1808
 
1809
        $result = core_course_external::get_course_contents($course->id);
1810
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1811
 
1812
        foreach ($result[0]['modules'] as $module) {
1813
            if ($module['modname'] == 'resource') {
1814
                $this->assertEmpty($module['dates']);
1815
            } else if ($module['modname'] == 'forum') {
1816
                $this->assertCount(1, $module['dates']);
1817
                $this->assertEquals('duedate', $module['dates'][0]['dataid']);
1818
                $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1819
            } else if ($module['modname'] == 'choice') {
1820
                $this->assertCount(2, $module['dates']);
1821
                $this->assertEquals('timeopen', $module['dates'][0]['dataid']);
1822
                $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1823
                $this->assertEquals('timeclose', $module['dates'][1]['dataid']);
1824
                $this->assertEquals($timenow + DAYSECS, $module['dates'][1]['timestamp']);
1825
            } else if ($module['modname'] == 'assign') {
1826
                $this->assertCount(1, $module['dates']);
1827
                $this->assertEquals('allowsubmissionsfromdate', $module['dates'][0]['dataid']);
1828
                $this->assertEquals($timenow, $module['dates'][0]['timestamp']);
1829
                $this->assertEquals($course->startdate, $module['dates'][0]['relativeto']);
1830
            }
1831
        }
1832
    }
1833
 
1834
    /**
1835
     * Test get_course_contents for courses with invalid course format.
1836
     */
11 efrain 1837
    public function test_get_course_contents_invalid_format(): void {
1 efrain 1838
        global $DB;
1839
        $this->resetAfterTest();
1840
 
1841
        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
1842
 
1843
        $DB->set_field('course', 'format', 'fakeformat', ['id' => $course->id]);
1844
 
1845
        // WS should falback to default course format (topics) and avoid exceptions (but debugging will happen).
1846
        $result = core_course_external::get_course_contents($course->id);
1847
        $this->assertDebuggingCalled();
1848
        $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
1849
    }
1850
 
1851
    /**
1852
     * Test duplicate_course
1853
     */
11 efrain 1854
    public function test_duplicate_course(): void {
1 efrain 1855
        $this->resetAfterTest(true);
1856
 
1857
        // Create one course with three modules.
1858
        $course  = self::getDataGenerator()->create_course();
1859
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1860
        $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
1861
        $forumcontext = context_module::instance($forum->cmid);
1862
        $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
1863
        $datacontext = context_module::instance($data->cmid);
1864
        $datacm = get_coursemodule_from_instance('page', $data->id);
1865
        $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
1866
        $pagecontext = context_module::instance($page->cmid);
1867
        $pagecm = get_coursemodule_from_instance('page', $page->id);
1868
 
1869
        // Set the required capabilities by the external function.
1870
        $coursecontext = context_course::instance($course->id);
1871
        $categorycontext = context_coursecat::instance($course->category);
1872
        $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
1873
        $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
1874
        $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
1875
        $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
1876
        $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
1877
        // Optional capabilities to copy user data.
1878
        $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
1879
        $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
1880
 
1881
        $newcourse['fullname'] = 'Course duplicate';
1882
        $newcourse['shortname'] = 'courseduplicate';
1883
        $newcourse['categoryid'] = $course->category;
1884
        $newcourse['visible'] = true;
1885
        $newcourse['options'][] = array('name' => 'users', 'value' => true);
1886
 
1887
        $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
1888
                $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
1889
 
1890
        // We need to execute the return values cleaning process to simulate the web service server.
1891
        $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
1892
 
1893
        // Check that the course has been duplicated.
1894
        $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
1895
    }
1896
 
1897
    /**
1898
     * Test update_courses
1899
     */
11 efrain 1900
    public function test_update_courses(): void {
1 efrain 1901
        global $DB, $CFG, $USER, $COURSE;
1902
 
1903
        // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
1904
        // trick because we are both updating and getting (for testing) course information
1905
        // in the same request and core_course_external::update_courses()
1906
        // is overwriting $COURSE all over the time with OLD values, so later
1907
        // use of get_course() fetches those OLD values instead of the updated ones.
1908
        // See MDL-39723 for more info.
1909
        $origcourse = clone($COURSE);
1910
 
1911
        $this->resetAfterTest(true);
1912
 
1913
        // Set the required capabilities by the external function.
1914
        $contextid = context_system::instance()->id;
1915
        $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
1916
        $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
1917
        $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
1918
        $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
1919
        $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
1920
        $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
1921
        $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
1922
        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
1923
        $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
1924
        $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
1925
 
1926
        // Create category and courses.
1927
        $category1  = self::getDataGenerator()->create_category();
1928
        $category2  = self::getDataGenerator()->create_category();
1929
 
1930
        $originalcourse1 = self::getDataGenerator()->create_course();
1931
        self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
1932
 
1933
        $originalcourse2 = self::getDataGenerator()->create_course();
1934
        self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
1935
 
1936
        // Course with custom fields.
1937
        $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
1938
 
1939
        $fieldtext = self::getDataGenerator()->create_custom_field([
1940
            'categoryid' => $fieldcategory->get('id'), 'name' => 'Text', 'shortname' => 'text', 'type' => 'text', 'configdata' => [
1941
                'locked' => 1,
1942
            ],
1943
        ]);
1944
        $fieldtextarea = self::getDataGenerator()->create_custom_field([
1945
            'categoryid' => $fieldcategory->get('id'), 'name' => 'Textarea', 'shortname' => 'textarea', 'type' => 'textarea',
1946
        ]);
1947
 
1948
        $originalcourse3 = self::getDataGenerator()->create_course();
1949
        self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid);
1950
 
1951
        // Course values to be updated.
1952
        $course1['id'] = $originalcourse1->id;
1953
        $course1['fullname'] = 'Updated test course 1';
1954
        $course1['shortname'] = 'Udestedtestcourse1';
1955
        $course1['categoryid'] = $category1->id;
1956
 
1957
        $course2['id'] = $originalcourse2->id;
1958
        $course2['fullname'] = 'Updated test course 2';
1959
        $course2['shortname'] = 'Updestedtestcourse2';
1960
        $course2['categoryid'] = $category2->id;
1961
        $course2['idnumber'] = 'Updatedidnumber2';
1962
        $course2['summary'] = 'Updaated description for course 2';
1963
        $course2['summaryformat'] = FORMAT_HTML;
1964
        $course2['format'] = 'topics';
1965
        $course2['showgrades'] = 1;
1966
        $course2['newsitems'] = 3;
1967
        $course2['startdate'] = 1420092000; // 01/01/2015.
1968
        $course2['enddate'] = 1422669600; // 01/31/2015.
1969
        $course2['maxbytes'] = 100000;
1970
        $course2['showreports'] = 1;
1971
        $course2['visible'] = 0;
1972
        $course2['hiddensections'] = 0;
1973
        $course2['groupmode'] = 0;
1974
        $course2['groupmodeforce'] = 0;
1975
        $course2['defaultgroupingid'] = 0;
1976
        $course2['enablecompletion'] = 1;
1977
        $course2['lang'] = 'en';
1978
        $course2['forcetheme'] = 'classic';
1979
 
1980
        $course3['id'] = $originalcourse3->id;
1981
        $course3['customfields'] = [
1982
            ['shortname' => $fieldtext->get('shortname'), 'value' => 'I long to see the sunlight in your hair'],
1983
            ['shortname' => $fieldtextarea->get('shortname'), 'value' => 'And tell you time and time again'],
1984
         ];
1985
 
1986
        $courses = array($course1, $course2, $course3);
1987
 
1988
        $updatedcoursewarnings = core_course_external::update_courses($courses);
1989
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
1990
                $updatedcoursewarnings);
1991
        $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
1992
 
1993
        // Check that right number of courses were created.
1994
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
1995
 
1996
        // Check that the courses were correctly created.
1997
        foreach ($courses as $course) {
1998
            $courseinfo = course_get_format($course['id'])->get_course();
1999
            $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']);
2000
            if ($course['id'] == $course2['id']) {
2001
                $this->assertEquals($course2['fullname'], $courseinfo->fullname);
2002
                $this->assertEquals($course2['shortname'], $courseinfo->shortname);
2003
                $this->assertEquals($course2['categoryid'], $courseinfo->category);
2004
                $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
2005
                $this->assertEquals($course2['summary'], $courseinfo->summary);
2006
                $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
2007
                $this->assertEquals($course2['format'], $courseinfo->format);
2008
                $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
2009
                $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
2010
                $this->assertEquals($course2['startdate'], $courseinfo->startdate);
2011
                $this->assertEquals($course2['enddate'], $courseinfo->enddate);
2012
                $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
2013
                $this->assertEquals($course2['showreports'], $courseinfo->showreports);
2014
                $this->assertEquals($course2['visible'], $courseinfo->visible);
2015
                $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
2016
                $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
2017
                $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
2018
                $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
2019
                $this->assertEquals($course2['lang'], $courseinfo->lang);
2020
 
2021
                if (!empty($CFG->allowcoursethemes)) {
2022
                    $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
2023
                }
2024
 
2025
                $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
2026
                $this->assertEquals((object) [
2027
                    'text' => null,
2028
                    'textarea' => null,
2029
                ], $customfields);
2030
            } else if ($course['id'] == $course1['id']) {
2031
                $this->assertEquals($course1['fullname'], $courseinfo->fullname);
2032
                $this->assertEquals($course1['shortname'], $courseinfo->shortname);
2033
                $this->assertEquals($course1['categoryid'], $courseinfo->category);
2034
                $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
2035
                $this->assertEquals('topics', $courseinfo->format);
2036
                $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
2037
                $this->assertEquals(0, $courseinfo->newsitems);
2038
                $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
2039
                $this->assertEquals((object) [
2040
                    'text' => null,
2041
                    'textarea' => null,
2042
                ], $customfields);
2043
            } else if ($course['id'] == $course3['id']) {
2044
                $this->assertEquals((object) [
2045
                    'text' => 'I long to see the sunlight in your hair',
2046
                    'textarea' => '<div class="text_to_html">And tell you time and time again</div>',
2047
                ], $customfields);
2048
            } else {
2049
                throw new moodle_exception('Unexpected shortname');
2050
            }
2051
        }
2052
 
2053
        $courses = array($course1);
2054
        // Try update course without update capability.
2055
        $user = self::getDataGenerator()->create_user();
2056
        $this->setUser($user);
2057
        $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
2058
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2059
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2060
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2061
                                                                    $updatedcoursewarnings);
2062
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2063
 
2064
        // Try update course category without capability.
2065
        $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
2066
        $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
2067
        $user = self::getDataGenerator()->create_user();
2068
        $this->setUser($user);
2069
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2070
        $course1['categoryid'] = $category2->id;
2071
        $courses = array($course1);
2072
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2073
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2074
                                                                    $updatedcoursewarnings);
2075
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2076
 
2077
        // Try update course fullname without capability.
2078
        $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
2079
        $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2080
        $user = self::getDataGenerator()->create_user();
2081
        $this->setUser($user);
2082
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2083
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2084
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2085
                                                                    $updatedcoursewarnings);
2086
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2087
        $course1['fullname'] = 'Testing fullname without permission';
2088
        $courses = array($course1);
2089
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2090
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2091
                                                                    $updatedcoursewarnings);
2092
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2093
 
2094
        // Try update course shortname without capability.
2095
        $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
2096
        $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2097
        $user = self::getDataGenerator()->create_user();
2098
        $this->setUser($user);
2099
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2100
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2101
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2102
                                                                    $updatedcoursewarnings);
2103
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2104
        $course1['shortname'] = 'Testing shortname without permission';
2105
        $courses = array($course1);
2106
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2107
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2108
                                                                    $updatedcoursewarnings);
2109
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2110
 
2111
        // Try update course idnumber without capability.
2112
        $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
2113
        $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2114
        $user = self::getDataGenerator()->create_user();
2115
        $this->setUser($user);
2116
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2117
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2118
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2119
                                                                    $updatedcoursewarnings);
2120
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2121
        $course1['idnumber'] = 'NEWIDNUMBER';
2122
        $courses = array($course1);
2123
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2124
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2125
                                                                    $updatedcoursewarnings);
2126
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2127
 
2128
        // Try update course summary without capability.
2129
        $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
2130
        $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2131
        $user = self::getDataGenerator()->create_user();
2132
        $this->setUser($user);
2133
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2134
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2135
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2136
                                                                    $updatedcoursewarnings);
2137
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2138
        $course1['summary'] = 'New summary';
2139
        $courses = array($course1);
2140
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2141
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2142
                                                                    $updatedcoursewarnings);
2143
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2144
 
2145
        // Try update course with invalid summary format.
2146
        $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
2147
        $user = self::getDataGenerator()->create_user();
2148
        $this->setUser($user);
2149
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2150
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2151
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2152
                                                                    $updatedcoursewarnings);
2153
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2154
        $course1['summaryformat'] = 10;
2155
        $courses = array($course1);
2156
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2157
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2158
                                                                    $updatedcoursewarnings);
2159
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2160
 
2161
        // Try update course visibility without capability.
2162
        $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
2163
        $user = self::getDataGenerator()->create_user();
2164
        $this->setUser($user);
2165
        self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
2166
        $course1['summaryformat'] = FORMAT_MOODLE;
2167
        $courses = array($course1);
2168
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2169
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2170
                                                                    $updatedcoursewarnings);
2171
        $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
2172
        $course1['visible'] = 0;
2173
        $courses = array($course1);
2174
        $updatedcoursewarnings = core_course_external::update_courses($courses);
2175
        $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
2176
                                                                    $updatedcoursewarnings);
2177
        $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
2178
 
2179
        // Try update course custom fields without capability.
2180
        $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
2181
        $user = self::getDataGenerator()->create_user();
2182
        $this->setUser($user);
2183
        self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid);
2184
 
2185
        $course3['customfields'] = [
2186
            ['shortname' => 'text', 'value' => 'New updated value'],
2187
        ];
2188
 
2189
        core_course_external::update_courses([$course3]);
2190
 
2191
        // Custom field was not updated.
2192
        $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']);
2193
        $this->assertEquals((object) [
2194
            'text' => 'I long to see the sunlight in your hair',
2195
            'textarea' => '<div class="text_to_html">And tell you time and time again</div>',
2196
        ], $customfields);
2197
    }
2198
 
2199
    /**
2200
     * Test delete course_module.
2201
     */
11 efrain 2202
    public function test_delete_modules(): void {
1 efrain 2203
        global $DB;
2204
 
2205
        // Ensure we reset the data after this test.
2206
        $this->resetAfterTest(true);
2207
 
2208
        // Create a user.
2209
        $user = self::getDataGenerator()->create_user();
2210
 
2211
        // Set the tests to run as the user.
2212
        self::setUser($user);
2213
 
2214
        // Create a course to add the modules.
2215
        $course = self::getDataGenerator()->create_course();
2216
 
2217
        // Create two test modules.
2218
        $record = new stdClass();
2219
        $record->course = $course->id;
2220
        $module1 = self::getDataGenerator()->create_module('forum', $record);
2221
        $module2 = self::getDataGenerator()->create_module('assign', $record);
2222
 
2223
        // Check the forum was correctly created.
2224
        $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
2225
 
2226
        // Check the assignment was correctly created.
2227
        $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
2228
 
2229
        // Check data exists in the course modules table.
2230
        $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2231
                array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2232
 
2233
        // Enrol the user in the course.
2234
        $enrol = enrol_get_plugin('manual');
2235
        $enrolinstances = enrol_get_instances($course->id, true);
2236
        foreach ($enrolinstances as $courseenrolinstance) {
2237
            if ($courseenrolinstance->enrol == "manual") {
2238
                $instance = $courseenrolinstance;
2239
                break;
2240
            }
2241
        }
2242
        $enrol->enrol_user($instance, $user->id);
2243
 
2244
        // Assign capabilities to delete module 1.
2245
        $modcontext = context_module::instance($module1->cmid);
2246
        $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
2247
 
2248
        // Assign capabilities to delete module 2.
2249
        $modcontext = context_module::instance($module2->cmid);
2250
        $newrole = create_role('Role 2', 'role2', 'Role 2 description');
2251
        $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
2252
 
2253
        // Deleting these module instances.
2254
        core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2255
 
2256
        // Check the forum was deleted.
2257
        $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
2258
 
2259
        // Check the assignment was deleted.
2260
        $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
2261
 
2262
        // Check we retrieve no data in the course modules table.
2263
        $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
2264
                array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
2265
 
2266
        // Call with non-existent course module id and ensure exception thrown.
2267
        try {
2268
            core_course_external::delete_modules(array('1337'));
2269
            $this->fail('Exception expected due to missing course module.');
2270
        } catch (dml_missing_record_exception $e) {
2271
            $this->assertEquals('invalidcoursemodule', $e->errorcode);
2272
        }
2273
 
2274
        // Create two modules.
2275
        $module1 = self::getDataGenerator()->create_module('forum', $record);
2276
        $module2 = self::getDataGenerator()->create_module('assign', $record);
2277
 
2278
        // Since these modules were recreated the user will not have capabilities
2279
        // to delete them, ensure exception is thrown if they try.
2280
        try {
2281
            core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2282
            $this->fail('Exception expected due to missing capability.');
2283
        } catch (moodle_exception $e) {
2284
            $this->assertEquals('nopermissions', $e->errorcode);
2285
        }
2286
 
2287
        // Unenrol user from the course.
2288
        $enrol->unenrol_user($instance, $user->id);
2289
 
2290
        // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
2291
        try {
2292
            core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
2293
            $this->fail('Exception expected due to being unenrolled from the course.');
2294
        } catch (moodle_exception $e) {
2295
            $this->assertEquals('requireloginerror', $e->errorcode);
2296
        }
2297
    }
2298
 
2299
    /**
2300
     * Test import_course into an empty course
2301
     */
11 efrain 2302
    public function test_import_course_empty(): void {
1 efrain 2303
        global $USER;
2304
 
2305
        $this->resetAfterTest(true);
2306
 
2307
        $course1  = self::getDataGenerator()->create_course();
2308
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
2309
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
2310
 
2311
        $course2  = self::getDataGenerator()->create_course();
2312
 
2313
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2314
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2315
 
2316
        // Verify the state of the courses before we do the import.
2317
        $this->assertCount(2, $course1cms);
2318
        $this->assertEmpty($course2cms);
2319
 
2320
        // Setup the user to run the operation (ugly hack because validate_context() will
2321
        // fail as the email is not set by $this->setAdminUser()).
2322
        $this->setAdminUser();
2323
        $USER->email = 'emailtopass@example.com';
2324
 
2325
        // Import from course1 to course2.
2326
        core_course_external::import_course($course1->id, $course2->id, 0);
2327
 
2328
        // Verify that now we have two modules in both courses.
2329
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2330
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2331
        $this->assertCount(2, $course1cms);
2332
        $this->assertCount(2, $course2cms);
2333
 
2334
        // Verify that the names transfered across correctly.
2335
        foreach ($course2cms as $cm) {
2336
            if ($cm->modname === 'page') {
2337
                $this->assertEquals($cm->name, $page->name);
2338
            } else if ($cm->modname === 'forum') {
2339
                $this->assertEquals($cm->name, $forum->name);
2340
            } else {
2341
                $this->fail('Unknown CM found.');
2342
            }
2343
        }
2344
    }
2345
 
2346
    /**
2347
     * Test import_course into an filled course
2348
     */
11 efrain 2349
    public function test_import_course_filled(): void {
1 efrain 2350
        global $USER;
2351
 
2352
        $this->resetAfterTest(true);
2353
 
2354
        // Add forum and page to course1.
2355
        $course1  = self::getDataGenerator()->create_course();
2356
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2357
        $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2358
 
2359
        // Add quiz to course 2.
2360
        $course2  = self::getDataGenerator()->create_course();
2361
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2362
 
2363
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2364
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2365
 
2366
        // Verify the state of the courses before we do the import.
2367
        $this->assertCount(2, $course1cms);
2368
        $this->assertCount(1, $course2cms);
2369
 
2370
        // Setup the user to run the operation (ugly hack because validate_context() will
2371
        // fail as the email is not set by $this->setAdminUser()).
2372
        $this->setAdminUser();
2373
        $USER->email = 'emailtopass@example.com';
2374
 
2375
        // Import from course1 to course2 without deleting content.
2376
        core_course_external::import_course($course1->id, $course2->id, 0);
2377
 
2378
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2379
 
2380
        // Verify that now we have three modules in course2.
2381
        $this->assertCount(3, $course2cms);
2382
 
2383
        // Verify that the names transfered across correctly.
2384
        foreach ($course2cms as $cm) {
2385
            if ($cm->modname === 'page') {
2386
                $this->assertEquals($cm->name, $page->name);
2387
            } else if ($cm->modname === 'forum') {
2388
                $this->assertEquals($cm->name, $forum->name);
2389
            } else if ($cm->modname === 'quiz') {
2390
                $this->assertEquals($cm->name, $quiz->name);
2391
            } else {
2392
                $this->fail('Unknown CM found.');
2393
            }
2394
        }
2395
    }
2396
 
2397
    /**
2398
     * Test import_course with only blocks set to backup
2399
     */
11 efrain 2400
    public function test_import_course_blocksonly(): void {
1 efrain 2401
        global $USER, $DB;
2402
 
2403
        $this->resetAfterTest(true);
2404
 
2405
        // Add forum and page to course1.
2406
        $course1  = self::getDataGenerator()->create_course();
2407
        $course1ctx = context_course::instance($course1->id);
2408
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2409
        $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
2410
 
2411
        $course2  = self::getDataGenerator()->create_course();
2412
        $course2ctx = context_course::instance($course2->id);
2413
        $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2414
        $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2415
 
2416
        // Setup the user to run the operation (ugly hack because validate_context() will
2417
        // fail as the email is not set by $this->setAdminUser()).
2418
        $this->setAdminUser();
2419
        $USER->email = 'emailtopass@example.com';
2420
 
2421
        // Import from course1 to course2 without deleting content, but excluding
2422
        // activities.
2423
        $options = array(
2424
            array('name' => 'activities', 'value' => 0),
2425
            array('name' => 'blocks', 'value' => 1),
2426
            array('name' => 'filters', 'value' => 0),
2427
        );
2428
 
2429
        core_course_external::import_course($course1->id, $course2->id, 0, $options);
2430
 
2431
        $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
2432
        $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
2433
        // Check that course modules haven't changed, but that blocks have.
2434
        $this->assertEquals($initialcmcount, $newcmcount);
2435
        $this->assertEquals(($initialblockcount + 1), $newblockcount);
2436
    }
2437
 
2438
    /**
2439
     * Test import_course into an filled course, deleting content.
2440
     */
11 efrain 2441
    public function test_import_course_deletecontent(): void {
1 efrain 2442
        global $USER;
2443
        $this->resetAfterTest(true);
2444
 
2445
        // Add forum and page to course1.
2446
        $course1  = self::getDataGenerator()->create_course();
2447
        $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
2448
        $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
2449
 
2450
        // Add quiz to course 2.
2451
        $course2  = self::getDataGenerator()->create_course();
2452
        $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
2453
 
2454
        $course1cms = get_fast_modinfo($course1->id)->get_cms();
2455
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2456
 
2457
        // Verify the state of the courses before we do the import.
2458
        $this->assertCount(2, $course1cms);
2459
        $this->assertCount(1, $course2cms);
2460
 
2461
        // Setup the user to run the operation (ugly hack because validate_context() will
2462
        // fail as the email is not set by $this->setAdminUser()).
2463
        $this->setAdminUser();
2464
        $USER->email = 'emailtopass@example.com';
2465
 
2466
        // Import from course1 to course2,  deleting content.
2467
        core_course_external::import_course($course1->id, $course2->id, 1);
2468
 
2469
        $course2cms = get_fast_modinfo($course2->id)->get_cms();
2470
 
2471
        // Verify that now we have two modules in course2.
2472
        $this->assertCount(2, $course2cms);
2473
 
2474
        // Verify that the course only contains the imported modules.
2475
        foreach ($course2cms as $cm) {
2476
            if ($cm->modname === 'page') {
2477
                $this->assertEquals($cm->name, $page->name);
2478
            } else if ($cm->modname === 'forum') {
2479
                $this->assertEquals($cm->name, $forum->name);
2480
            } else {
2481
                $this->fail('Unknown CM found: '.$cm->name);
2482
            }
2483
        }
2484
    }
2485
 
2486
    /**
2487
     * Ensure import_course handles incorrect deletecontent option correctly.
2488
     */
11 efrain 2489
    public function test_import_course_invalid_deletecontent_option(): void {
1 efrain 2490
        $this->resetAfterTest(true);
2491
 
2492
        $course1  = self::getDataGenerator()->create_course();
2493
        $course2  = self::getDataGenerator()->create_course();
2494
 
2495
        $this->expectException('moodle_exception');
2496
        $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
2497
        // Import from course1 to course2, with invalid option
2498
        core_course_external::import_course($course1->id, $course2->id, -1);;
2499
    }
2500
 
2501
    /**
2502
     * Test view_course function
2503
     */
11 efrain 2504
    public function test_view_course(): void {
1 efrain 2505
 
2506
        $this->resetAfterTest();
2507
 
2508
        // Course without sections.
2509
        $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
2510
        $this->setAdminUser();
2511
 
2512
        // Redirect events to the sink, so we can recover them later.
2513
        $sink = $this->redirectEvents();
2514
 
2515
        $result = core_course_external::view_course($course->id, 1);
2516
        $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2517
        $events = $sink->get_events();
2518
        $event = reset($events);
2519
 
2520
        // Check the event details are correct.
2521
        $this->assertInstanceOf('\core\event\course_viewed', $event);
2522
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
2523
        $this->assertEquals(1, $event->other['coursesectionnumber']);
2524
 
2525
        $result = core_course_external::view_course($course->id);
2526
        $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
2527
        $events = $sink->get_events();
2528
        $event = array_pop($events);
2529
        $sink->close();
2530
 
2531
        // Check the event details are correct.
2532
        $this->assertInstanceOf('\core\event\course_viewed', $event);
2533
        $this->assertEquals(context_course::instance($course->id), $event->get_context());
2534
        $this->assertEmpty($event->other);
2535
 
2536
    }
2537
 
2538
    /**
2539
     * Test get_course_module
2540
     */
11 efrain 2541
    public function test_get_course_module(): void {
1 efrain 2542
        global $DB;
2543
 
2544
        $this->resetAfterTest(true);
2545
 
2546
        $this->setAdminUser();
2547
        $course = self::getDataGenerator()->create_course(['enablecompletion' => 1]);
2548
        $record = array(
2549
            'course' => $course->id,
2550
            'name' => 'First Assignment'
2551
        );
2552
        $options = array(
2553
            'idnumber' => 'ABC',
2554
            'visible' => 0,
2555
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
2556
            'completiongradeitemnumber' => 0,
2557
            'completionpassgrade' => 1,
2558
        );
2559
        // Hidden activity.
2560
        $assign = self::getDataGenerator()->create_module('assign', $record, $options);
2561
 
2562
        $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
2563
 
2564
        // Insert a custom grade scale to be used by an outcome.
2565
        $gradescale = new grade_scale();
2566
        $gradescale->name        = 'gettcoursemodulescale';
2567
        $gradescale->courseid    = $course->id;
2568
        $gradescale->userid      = 0;
2569
        $gradescale->scale       = $outcomescale;
2570
        $gradescale->description = 'This scale is used to mark standard assignments.';
2571
        $gradescale->insert();
2572
 
2573
        // Insert an outcome.
2574
        $data = new stdClass();
2575
        $data->courseid = $course->id;
2576
        $data->fullname = 'Team work';
2577
        $data->shortname = 'Team work';
2578
        $data->scaleid = $gradescale->id;
2579
        $outcome = new grade_outcome($data, false);
2580
        $outcome->insert();
2581
 
2582
        $outcomegradeitem = new grade_item();
2583
        $outcomegradeitem->itemname = $outcome->shortname;
2584
        $outcomegradeitem->itemtype = 'mod';
2585
        $outcomegradeitem->itemmodule = 'assign';
2586
        $outcomegradeitem->iteminstance = $assign->id;
2587
        $outcomegradeitem->outcomeid = $outcome->id;
2588
        $outcomegradeitem->cmid = 0;
2589
        $outcomegradeitem->courseid = $course->id;
2590
        $outcomegradeitem->aggregationcoef = 0;
2591
        $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
2592
        $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
2593
        $outcomegradeitem->scaleid = $outcome->scaleid;
2594
        $outcomegradeitem->insert();
2595
 
2596
        $assignmentgradeitem = grade_item::fetch(
2597
            array(
2598
                'itemtype' => 'mod',
2599
                'itemmodule' => 'assign',
2600
                'iteminstance' => $assign->id,
2601
                'itemnumber' => 0,
2602
                'courseid' => $course->id
2603
            )
2604
        );
2605
        $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
2606
        $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
2607
 
2608
        // Test admin user can see the complete hidden activity.
2609
        $result = core_course_external::get_course_module($assign->cmid);
2610
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2611
 
2612
        $this->assertCount(0, $result['warnings']);
2613
        // Test we retrieve all the fields.
2614
        $this->assertCount(30, $result['cm']);
2615
        $this->assertEquals($record['name'], $result['cm']['name']);
2616
        $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2617
        $this->assertEquals(100, $result['cm']['grade']);
2618
        $this->assertEquals(0.0, $result['cm']['gradepass']);
2619
        $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
2620
        $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
2621
        $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
2622
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2623
 
2624
        $student = $this->getDataGenerator()->create_user();
2625
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2626
 
2627
        self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2628
        $this->setUser($student);
2629
 
2630
        // The user shouldn't be able to see the activity.
2631
        try {
2632
            core_course_external::get_course_module($assign->cmid);
2633
            $this->fail('Exception expected due to invalid permissions.');
2634
        } catch (moodle_exception $e) {
2635
            $this->assertEquals('requireloginerror', $e->errorcode);
2636
        }
2637
 
2638
        // Make module visible.
2639
        set_coursemodule_visible($assign->cmid, 1);
2640
 
2641
        // Test student user.
2642
        $result = core_course_external::get_course_module($assign->cmid);
2643
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
2644
 
2645
        $this->assertCount(0, $result['warnings']);
2646
        // Test we retrieve only the few files we can see.
2647
        $this->assertCount(12, $result['cm']);
2648
        $this->assertEquals($assign->cmid, $result['cm']['id']);
2649
        $this->assertEquals($course->id, $result['cm']['course']);
2650
        $this->assertEquals('assign', $result['cm']['modname']);
2651
        $this->assertEquals($assign->id, $result['cm']['instance']);
2652
 
2653
    }
2654
 
2655
    /**
2656
     * Test get_course_module_by_instance
2657
     */
11 efrain 2658
    public function test_get_course_module_by_instance(): void {
1 efrain 2659
        global $DB;
2660
 
2661
        $this->resetAfterTest(true);
2662
 
2663
        $this->setAdminUser();
2664
        $course = self::getDataGenerator()->create_course();
2665
        $record = array(
2666
            'course' => $course->id,
2667
            'name' => 'First quiz',
2668
            'grade' => 90.00
2669
        );
2670
        $options = array(
2671
            'idnumber' => 'ABC',
2672
            'visible' => 0
2673
        );
2674
        // Hidden activity.
2675
        $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
2676
 
2677
        // Test admin user can see the complete hidden activity.
2678
        $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2679
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2680
 
2681
        $this->assertCount(0, $result['warnings']);
2682
        // Test we retrieve all the fields.
2683
        $this->assertCount(28, $result['cm']);
2684
        $this->assertEquals($record['name'], $result['cm']['name']);
2685
        $this->assertEquals($record['grade'], $result['cm']['grade']);
2686
        $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
2687
        $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']);
2688
 
2689
        $student = $this->getDataGenerator()->create_user();
2690
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2691
 
2692
        self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
2693
        $this->setUser($student);
2694
 
2695
        // The user shouldn't be able to see the activity.
2696
        try {
2697
            core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2698
            $this->fail('Exception expected due to invalid permissions.');
2699
        } catch (moodle_exception $e) {
2700
            $this->assertEquals('requireloginerror', $e->errorcode);
2701
        }
2702
 
2703
        // Make module visible.
2704
        set_coursemodule_visible($quiz->cmid, 1);
2705
 
2706
        // Test student user.
2707
        $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
2708
        $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
2709
 
2710
        $this->assertCount(0, $result['warnings']);
2711
        // Test we retrieve only the few files we can see.
2712
        $this->assertCount(12, $result['cm']);
2713
        $this->assertEquals($quiz->cmid, $result['cm']['id']);
2714
        $this->assertEquals($course->id, $result['cm']['course']);
2715
        $this->assertEquals('quiz', $result['cm']['modname']);
2716
        $this->assertEquals($quiz->id, $result['cm']['instance']);
2717
 
2718
        // Try with an invalid module name.
2719
        try {
2720
            core_course_external::get_course_module_by_instance('abc', $quiz->id);
2721
            $this->fail('Exception expected due to invalid module name.');
2722
        } catch (dml_read_exception $e) {
2723
            $this->assertEquals('dmlreadexception', $e->errorcode);
2724
        }
2725
 
2726
    }
2727
 
2728
    /**
2729
     * Test get_user_navigation_options
2730
     */
11 efrain 2731
    public function test_get_user_navigation_options(): void {
1 efrain 2732
        global $USER;
2733
 
2734
        $this->resetAfterTest();
2735
        $course1 = self::getDataGenerator()->create_course();
2736
        $course2 = self::getDataGenerator()->create_course();
2737
 
2738
        // Create a viewer user.
2739
        $viewer = self::getDataGenerator()->create_user();
2740
        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2741
        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2742
 
2743
        $this->setUser($viewer->id);
2744
        $courses = array($course1->id , $course2->id, SITEID);
2745
 
2746
        $result = core_course_external::get_user_navigation_options($courses);
2747
        $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
2748
 
2749
        $this->assertCount(0, $result['warnings']);
2750
        $this->assertCount(3, $result['courses']);
2751
 
2752
        foreach ($result['courses'] as $course) {
2753
            $navoptions = new stdClass;
2754
            foreach ($course['options'] as $option) {
2755
                $navoptions->{$option['name']} = $option['available'];
2756
            }
2757
            $this->assertCount(9, $course['options']);
2758
            if ($course['id'] == SITEID) {
2759
                $this->assertTrue($navoptions->blogs);
2760
                $this->assertFalse($navoptions->notes);
2761
                $this->assertFalse($navoptions->participants);
2762
                $this->assertTrue($navoptions->badges);
2763
                $this->assertTrue($navoptions->tags);
2764
                $this->assertFalse($navoptions->grades);
2765
                $this->assertFalse($navoptions->search);
2766
                $this->assertTrue($navoptions->competencies);
2767
                $this->assertFalse($navoptions->communication);
2768
            } else {
2769
                $this->assertTrue($navoptions->blogs);
2770
                $this->assertFalse($navoptions->notes);
2771
                $this->assertTrue($navoptions->participants);
2772
                $this->assertFalse($navoptions->badges);
2773
                $this->assertFalse($navoptions->tags);
2774
                $this->assertTrue($navoptions->grades);
2775
                $this->assertFalse($navoptions->search);
2776
                $this->assertTrue($navoptions->competencies);
2777
                $this->assertFalse($navoptions->communication);
2778
            }
2779
        }
2780
    }
2781
 
2782
    /**
2783
     * Test get_user_administration_options
2784
     */
11 efrain 2785
    public function test_get_user_administration_options(): void {
1 efrain 2786
        global $USER;
2787
 
2788
        $this->resetAfterTest();
2789
        $course1 = self::getDataGenerator()->create_course();
2790
        $course2 = self::getDataGenerator()->create_course();
2791
 
2792
        // Create a viewer user.
2793
        $viewer = self::getDataGenerator()->create_user();
2794
        $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
2795
        $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
2796
 
2797
        $this->setUser($viewer->id);
2798
        $courses = array($course1->id , $course2->id, SITEID);
2799
 
2800
        $result = core_course_external::get_user_administration_options($courses);
2801
        $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
2802
 
2803
        $this->assertCount(0, $result['warnings']);
2804
        $this->assertCount(3, $result['courses']);
2805
 
2806
        foreach ($result['courses'] as $course) {
2807
            $adminoptions = new stdClass;
2808
            foreach ($course['options'] as $option) {
2809
                $adminoptions->{$option['name']} = $option['available'];
2810
            }
2811
            if ($course['id'] == SITEID) {
2812
                $this->assertCount(17, $course['options']);
2813
                $this->assertFalse($adminoptions->update);
2814
                $this->assertFalse($adminoptions->filters);
2815
                $this->assertFalse($adminoptions->reports);
2816
                $this->assertFalse($adminoptions->backup);
2817
                $this->assertFalse($adminoptions->restore);
2818
                $this->assertFalse($adminoptions->files);
2819
                $this->assertFalse(!isset($adminoptions->tags));
2820
                $this->assertFalse($adminoptions->gradebook);
2821
                $this->assertFalse($adminoptions->outcomes);
2822
                $this->assertFalse($adminoptions->badges);
2823
                $this->assertFalse($adminoptions->import);
2824
                $this->assertFalse($adminoptions->reset);
2825
                $this->assertFalse($adminoptions->roles);
2826
                $this->assertFalse($adminoptions->editcompletion);
2827
                $this->assertFalse($adminoptions->copy);
2828
            } else {
2829
                $this->assertCount(15, $course['options']);
2830
                $this->assertFalse($adminoptions->update);
2831
                $this->assertFalse($adminoptions->filters);
2832
                $this->assertFalse($adminoptions->reports);
2833
                $this->assertFalse($adminoptions->backup);
2834
                $this->assertFalse($adminoptions->restore);
2835
                $this->assertFalse($adminoptions->files);
2836
                $this->assertFalse($adminoptions->tags);
2837
                $this->assertFalse($adminoptions->gradebook);
2838
                $this->assertFalse($adminoptions->outcomes);
2839
                $this->assertTrue($adminoptions->badges);
2840
                $this->assertFalse($adminoptions->import);
2841
                $this->assertFalse($adminoptions->reset);
2842
                $this->assertFalse($adminoptions->roles);
2843
                $this->assertFalse($adminoptions->editcompletion);
2844
                $this->assertFalse($adminoptions->copy);
2845
            }
2846
        }
2847
    }
2848
 
2849
    /**
2850
     * Test get_courses_by_fields
2851
     */
11 efrain 2852
    public function test_get_courses_by_field(): void {
1 efrain 2853
        global $DB, $USER;
2854
        $this->resetAfterTest(true);
2855
 
2856
        $this->setAdminUser();
2857
 
2858
        $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
2859
        $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
2860
        $course1 = self::getDataGenerator()->create_course(
2861
            array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
2862
 
2863
        $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
2864
        $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
2865
            'categoryid' => $fieldcategory->get('id')];
2866
        $field = self::getDataGenerator()->create_custom_field($customfield);
2867
        $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
2868
        // Create course image.
2869
        $draftid = file_get_unused_draft_itemid();
2870
        $filerecord = [
2871
            'component' => 'user',
2872
            'filearea' => 'draft',
2873
            'contextid' => context_user::instance($USER->id)->id,
2874
            'itemid' => $draftid,
2875
            'filename' => 'image.jpg',
2876
            'filepath' => '/',
2877
        ];
2878
        $fs = get_file_storage();
2879
        $fs->create_file_from_pathname($filerecord, __DIR__ . '/fixtures/image.jpg');
2880
        $course2 = self::getDataGenerator()->create_course([
2881
            'visible' => 0,
2882
            'category' => $category2->id,
2883
            'idnumber' => 'i2',
2884
            'customfields' => [$customfieldvalue],
2885
            'overviewfiles_filemanager' => $draftid
2886
        ]);
2887
 
2888
        $student1 = self::getDataGenerator()->create_user();
2889
        $user1 = self::getDataGenerator()->create_user();
2890
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2891
        self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id);
2892
        self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id);
2893
 
2894
        self::setAdminUser();
2895
        // As admins, we should be able to retrieve everything.
2896
        $result = core_course_external::get_courses_by_field();
2897
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2898
        $this->assertCount(3, $result['courses']);
2899
        // Expect to receive all the fields.
2900
        $this->assertCount(41, $result['courses'][0]);
2901
        $this->assertCount(42, $result['courses'][1]);  // One more field because is not the site course.
2902
        $this->assertCount(42, $result['courses'][2]);  // One more field because is not the site course.
2903
 
2904
        $result = core_course_external::get_courses_by_field('id', $course1->id);
2905
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2906
        $this->assertCount(1, $result['courses']);
2907
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
2908
        // Expect to receive all the fields.
2909
        $this->assertCount(42, $result['courses'][0]);
2910
        // Check default values for course format topics.
2911
        $this->assertCount(3, $result['courses'][0]['courseformatoptions']);
2912
        foreach ($result['courses'][0]['courseformatoptions'] as $option) {
2913
            switch ($option['name']) {
2914
                case 'hiddensections':
2915
                    $this->assertEquals(1, $option['value']);
2916
                    break;
2917
                case 'coursedisplay':
2918
                    $this->assertEquals(0, $option['value']);
2919
                    break;
2920
                case 'indentation':
2921
                    $this->assertEquals(1, $option['value']);
2922
                    break;
2923
                default:
2924
            }
2925
        }
2926
        $this->assertStringContainsString('/course/generated', $result['courses'][0]['courseimage']);
2927
 
2928
        $result = core_course_external::get_courses_by_field('id', $course2->id);
2929
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2930
        $this->assertCount(1, $result['courses']);
2931
        $this->assertEquals($course2->id, $result['courses'][0]['id']);
2932
        // Check custom fields properly returned.
2933
        $this->assertEquals([
2934
            'shortname' => $customfield['shortname'],
2935
            'name' => $customfield['name'],
2936
            'type' => $customfield['type'],
2937
            'value' => $customfieldvalue['value'],
2938
            'valueraw' => $customfieldvalue['value'],
2939
        ], $result['courses'][0]['customfields'][0]);
2940
        $this->assertStringContainsString('/course/overviewfiles', $result['courses'][0]['courseimage']);
2941
 
2942
        $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
2943
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2944
        $this->assertCount(2, $result['courses']);
2945
 
2946
        // Check default filters.
2947
        $this->assertCount(6, $result['courses'][0]['filters']);
2948
        $this->assertCount(6, $result['courses'][1]['filters']);
2949
 
2950
        $result = core_course_external::get_courses_by_field('category', $category1->id);
2951
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2952
        $this->assertCount(1, $result['courses']);
2953
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
2954
        $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']);
2955
 
2956
        $result = core_course_external::get_courses_by_field('shortname', 'c1');
2957
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2958
        $this->assertCount(1, $result['courses']);
2959
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
2960
 
2961
        $result = core_course_external::get_courses_by_field('idnumber', 'i2');
2962
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2963
        $this->assertCount(1, $result['courses']);
2964
        $this->assertEquals($course2->id, $result['courses'][0]['id']);
2965
 
2966
        $result = core_course_external::get_courses_by_field('idnumber', 'x');
2967
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2968
        $this->assertCount(0, $result['courses']);
2969
 
2970
        // Change filter value.
2971
        filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF);
2972
 
2973
        self::setUser($student1);
2974
        // All visible courses  (including front page) for normal student.
2975
        $result = core_course_external::get_courses_by_field();
2976
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2977
        $this->assertCount(2, $result['courses']);
2978
        $this->assertCount(34, $result['courses'][0]);
2979
        $this->assertCount(35, $result['courses'][1]);  // One field more (course format options), not present in site course.
2980
 
2981
        $result = core_course_external::get_courses_by_field('id', $course1->id);
2982
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
2983
        $this->assertCount(1, $result['courses']);
2984
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
2985
        // Expect to receive all the files that a student can see.
2986
        $this->assertCount(35, $result['courses'][0]);
2987
 
2988
        // Check default filters.
2989
        $filters = $result['courses'][0]['filters'];
2990
        $this->assertCount(6, $filters);
2991
        $found = false;
2992
        foreach ($filters as $filter) {
2993
            if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
2994
                $found = true;
2995
            }
2996
        }
2997
        $this->assertTrue($found);
2998
 
2999
        // Course 2 is not visible.
3000
        $result = core_course_external::get_courses_by_field('id', $course2->id);
3001
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3002
        $this->assertCount(0, $result['courses']);
3003
 
3004
        $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
3005
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3006
        $this->assertCount(1, $result['courses']);
3007
 
3008
        $result = core_course_external::get_courses_by_field('category', $category1->id);
3009
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3010
        $this->assertCount(1, $result['courses']);
3011
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3012
 
3013
        $result = core_course_external::get_courses_by_field('shortname', 'c1');
3014
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3015
        $this->assertCount(1, $result['courses']);
3016
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3017
 
3018
        $result = core_course_external::get_courses_by_field('idnumber', 'i2');
3019
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3020
        $this->assertCount(0, $result['courses']);
3021
 
3022
        $result = core_course_external::get_courses_by_field('idnumber', 'x');
3023
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3024
        $this->assertCount(0, $result['courses']);
3025
 
3026
        self::setUser($user1);
3027
        // All visible courses (including front page) for authenticated user.
3028
        $result = core_course_external::get_courses_by_field();
3029
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3030
        $this->assertCount(2, $result['courses']);
3031
        $this->assertCount(34, $result['courses'][0]);  // Site course.
3032
        $this->assertCount(17, $result['courses'][1]);  // Only public information, not enrolled.
3033
 
3034
        $result = core_course_external::get_courses_by_field('id', $course1->id);
3035
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3036
        $this->assertCount(1, $result['courses']);
3037
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3038
        // Expect to receive all the files that a authenticated can see.
3039
        $this->assertCount(17, $result['courses'][0]);
3040
 
3041
        // Course 2 is not visible.
3042
        $result = core_course_external::get_courses_by_field('id', $course2->id);
3043
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3044
        $this->assertCount(0, $result['courses']);
3045
 
3046
        $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id");
3047
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3048
        $this->assertCount(1, $result['courses']);
3049
 
3050
        $result = core_course_external::get_courses_by_field('category', $category1->id);
3051
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3052
        $this->assertCount(1, $result['courses']);
3053
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3054
 
3055
        $result = core_course_external::get_courses_by_field('shortname', 'c1');
3056
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3057
        $this->assertCount(1, $result['courses']);
3058
        $this->assertEquals($course1->id, $result['courses'][0]['id']);
3059
 
3060
        $result = core_course_external::get_courses_by_field('idnumber', 'i2');
3061
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3062
        $this->assertCount(0, $result['courses']);
3063
 
3064
        $result = core_course_external::get_courses_by_field('idnumber', 'x');
3065
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3066
        $this->assertCount(0, $result['courses']);
3067
    }
3068
 
3069
    /**
3070
     * Test retrieving courses by field returns custom field data
3071
     */
3072
    public function test_get_courses_by_field_customfields(): void {
3073
        $this->resetAfterTest();
3074
        $this->setAdminUser();
3075
 
3076
        $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
3077
        $datefield = $this->getDataGenerator()->create_custom_field([
3078
            'categoryid' => $fieldcategory->get('id'),
3079
            'shortname' => 'mydate',
3080
            'name' => 'My date',
3081
            'type' => 'date',
3082
        ]);
3083
 
3084
        $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
3085
            [
3086
                'shortname' => $datefield->get('shortname'),
3087
                'value' => 1580389200, // 30/01/2020 13:00 GMT.
3088
            ],
3089
        ]]);
3090
 
3091
        $result = external_api::clean_returnvalue(
3092
            core_course_external::get_courses_by_field_returns(),
3093
            core_course_external::get_courses_by_field('id', $newcourse->id)
3094
        );
3095
 
3096
        $this->assertCount(1, $result['courses']);
3097
        $course = reset($result['courses']);
3098
 
3099
        $this->assertArrayHasKey('customfields', $course);
3100
        $this->assertCount(1, $course['customfields']);
3101
 
3102
        // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
3103
        $this->assertEquals([
3104
            'name' => $datefield->get('name'),
3105
            'shortname' => $datefield->get('shortname'),
3106
            'type' => $datefield->get('type'),
3107
            'value' => userdate(1580389200),
3108
            'valueraw' => 1580389200,
3109
        ], reset($course['customfields']));
3110
    }
3111
 
3112
    /**
3113
     * Test retrieving courses by field returning communication tools.
3114
     * @covers \core_course_external::get_courses_by_field
3115
     */
3116
    public function test_get_courses_by_field_communication(): void {
3117
        $this->resetAfterTest();
3118
        $this->setAdminUser();
3119
 
3120
        // Create communication tool in course.
3121
        set_config('enablecommunicationsubsystem', 1);
3122
 
3123
        $roomname = 'Course chat';
3124
        $telegramlink = 'https://my.telegram.chat/120';
3125
        $record = [
3126
            'selectedcommunication' => 'communication_customlink',
3127
            'communicationroomname' => $roomname,
3128
            'customlinkurl' => $telegramlink,
3129
        ];
3130
        $course = $this->getDataGenerator()->create_course($record);
3131
        $communication = \core_communication\api::load_by_instance(
3132
            context: \core\context\course::instance($course->id),
3133
            component: 'core_course',
3134
            instancetype: 'coursecommunication',
3135
            instanceid: $course->id,
3136
        );
3137
 
3138
        $result = external_api::clean_returnvalue(
3139
            core_course_external::get_courses_by_field_returns(),
3140
            core_course_external::get_courses_by_field('id', $course->id)
3141
        );
3142
 
3143
        $course = reset($result['courses']);
3144
        $this->assertEquals($roomname, $course['communicationroomname']);
3145
        $this->assertEquals($telegramlink, $course['communicationroomurl']);
3146
 
3147
        // Course without comm tools.
3148
        $course = $this->getDataGenerator()->create_course();
3149
        $result = external_api::clean_returnvalue(
3150
            core_course_external::get_courses_by_field_returns(),
3151
            core_course_external::get_courses_by_field('id', $course->id)
3152
        );
3153
 
3154
        $course = reset($result['courses']);
3155
        $this->assertNotContains('communicationroomname', $course);
3156
        $this->assertNotContains('communicationroomurl', $course);
3157
    }
3158
 
11 efrain 3159
    public function test_get_courses_by_field_invalid_field(): void {
1 efrain 3160
        $this->expectException('invalid_parameter_exception');
3161
        $result = core_course_external::get_courses_by_field('zyx', 'x');
3162
    }
3163
 
11 efrain 3164
    public function test_get_courses_by_field_invalid_courses(): void {
1 efrain 3165
        $result = core_course_external::get_courses_by_field('id', '-1');
3166
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3167
        $this->assertCount(0, $result['courses']);
3168
    }
3169
 
3170
    /**
3171
     * Test get_courses_by_field_invalid_theme_and_lang
3172
     */
11 efrain 3173
    public function test_get_courses_by_field_invalid_theme_and_lang(): void {
1 efrain 3174
        $this->resetAfterTest(true);
3175
        $this->setAdminUser();
3176
 
3177
        $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
3178
        $result = core_course_external::get_courses_by_field('id', $course->id);
3179
        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
3180
        $this->assertEmpty($result['courses']['0']['theme']);
3181
        $this->assertEmpty($result['courses']['0']['lang']);
3182
    }
3183
 
3184
 
11 efrain 3185
    public function test_check_updates(): void {
1 efrain 3186
        global $DB;
3187
        $this->resetAfterTest(true);
3188
        $this->setAdminUser();
3189
 
3190
        // Create different types of activities.
3191
        $course  = self::getDataGenerator()->create_course();
3192
        $tocreate = [
3193
            'assign',
3194
            'book',
3195
            'choice',
3196
            'folder',
3197
            'forum',
3198
            'glossary',
3199
            'imscp',
3200
            'label',
3201
            'lesson',
3202
            'lti',
3203
            'page',
3204
            'quiz',
3205
            'resource',
3206
            'scorm',
3207
            'url',
3208
            'wiki',
3209
        ];
3210
 
3211
        $modules = array();
3212
        foreach ($tocreate as $modname) {
3213
            $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id));
3214
            $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid);
3215
            $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid);
3216
        }
3217
 
3218
        $student = self::getDataGenerator()->create_user();
3219
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
3220
        self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
3221
        $this->setUser($student);
3222
 
3223
        $since = time();
3224
        $this->waitForSecond();
3225
        $params = array();
3226
        foreach ($modules as $modname => $data) {
3227
            $params[$data['cm']->id] = array(
3228
                'contextlevel' => 'module',
3229
                'id' => $data['cm']->id,
3230
                'since' => $since
3231
            );
3232
        }
3233
 
3234
        // Check there is nothing updated because modules are fresh new.
3235
        $result = core_course_external::check_updates($course->id, $params);
3236
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3237
        $this->assertCount(0, $result['instances']);
3238
        $this->assertCount(0, $result['warnings']);
3239
 
3240
        // Test with get_updates_since the same data.
3241
        $result = core_course_external::get_updates_since($course->id, $since);
3242
        $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3243
        $this->assertCount(0, $result['instances']);
3244
        $this->assertCount(0, $result['warnings']);
3245
 
3246
        // Update a module after a second.
3247
        $this->waitForSecond();
3248
        set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
3249
 
3250
        $found = false;
3251
        $result = core_course_external::check_updates($course->id, $params);
3252
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3253
        $this->assertCount(1, $result['instances']);
3254
        $this->assertCount(0, $result['warnings']);
3255
        foreach ($result['instances'] as $module) {
3256
            foreach ($module['updates'] as $update) {
3257
                if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3258
                    $found = true;
3259
                }
3260
            }
3261
        }
3262
        $this->assertTrue($found);
3263
 
3264
        // Test with get_updates_since the same data.
3265
        $result = core_course_external::get_updates_since($course->id, $since);
3266
        $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
3267
        $this->assertCount(1, $result['instances']);
3268
        $this->assertCount(0, $result['warnings']);
3269
        $found = false;
3270
        $this->assertCount(1, $result['instances']);
3271
        $this->assertCount(0, $result['warnings']);
3272
        foreach ($result['instances'] as $module) {
3273
            foreach ($module['updates'] as $update) {
3274
                if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
3275
                    $found = true;
3276
                }
3277
            }
3278
        }
3279
        $this->assertTrue($found);
3280
 
3281
        // Do not retrieve the configuration field.
3282
        $filter = array('files');
3283
        $found = false;
3284
        $result = core_course_external::check_updates($course->id, $params, $filter);
3285
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3286
        $this->assertCount(0, $result['instances']);
3287
        $this->assertCount(0, $result['warnings']);
3288
        $this->assertFalse($found);
3289
 
3290
        // Add invalid cmid.
3291
        $params[] = array(
3292
            'contextlevel' => 'module',
3293
            'id' => -2,
3294
            'since' => $since
3295
        );
3296
        $result = core_course_external::check_updates($course->id, $params);
3297
        $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result);
3298
        $this->assertCount(1, $result['warnings']);
3299
        $this->assertEquals(-2, $result['warnings'][0]['itemid']);
3300
    }
3301
 
3302
    /**
3303
     * Test cases for the get_enrolled_courses_by_timeline_classification test.
3304
     */
11 efrain 3305
    public static function get_get_enrolled_courses_by_timeline_classification_test_cases(): array {
1 efrain 3306
        $now = time();
3307
        $day = 86400;
3308
 
3309
        $coursedata = [
3310
            [
3311
                'shortname' => 'apast',
3312
                'startdate' => $now - ($day * 2),
3313
                'enddate' => $now - $day
3314
            ],
3315
            [
3316
                'shortname' => 'bpast',
3317
                'startdate' => $now - ($day * 2),
3318
                'enddate' => $now - $day
3319
            ],
3320
            [
3321
                'shortname' => 'cpast',
3322
                'startdate' => $now - ($day * 2),
3323
                'enddate' => $now - $day
3324
            ],
3325
            [
3326
                'shortname' => 'dpast',
3327
                'startdate' => $now - ($day * 2),
3328
                'enddate' => $now - $day
3329
            ],
3330
            [
3331
                'shortname' => 'epast',
3332
                'startdate' => $now - ($day * 2),
3333
                'enddate' => $now - $day
3334
            ],
3335
            [
3336
                'shortname' => 'ainprogress',
3337
                'startdate' => $now - $day,
3338
                'enddate' => $now + $day
3339
            ],
3340
            [
3341
                'shortname' => 'binprogress',
3342
                'startdate' => $now - $day,
3343
                'enddate' => $now + $day
3344
            ],
3345
            [
3346
                'shortname' => 'cinprogress',
3347
                'startdate' => $now - $day,
3348
                'enddate' => $now + $day
3349
            ],
3350
            [
3351
                'shortname' => 'dinprogress',
3352
                'startdate' => $now - $day,
3353
                'enddate' => $now + $day
3354
            ],
3355
            [
3356
                'shortname' => 'einprogress',
3357
                'startdate' => $now - $day,
3358
                'enddate' => $now + $day
3359
            ],
3360
            [
3361
                'shortname' => 'afuture',
3362
                'startdate' => $now + $day
3363
            ],
3364
            [
3365
                'shortname' => 'bfuture',
3366
                'startdate' => $now + $day
3367
            ],
3368
            [
3369
                'shortname' => 'cfuture',
3370
                'startdate' => $now + $day
3371
            ],
3372
            [
3373
                'shortname' => 'dfuture',
3374
                'startdate' => $now + $day
3375
            ],
3376
            [
3377
                'shortname' => 'efuture',
3378
                'startdate' => $now + $day
3379
            ]
3380
        ];
3381
 
3382
        // Raw enrolled courses result set should be returned in this order:
3383
        // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
3384
        // dfuture, dinprogress, dpast, efuture, einprogress, epast
3385
        //
3386
        // By classification the offset values for each record should be:
3387
        // COURSE_TIMELINE_FUTURE
3388
        // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
3389
        // COURSE_TIMELINE_INPROGRESS
3390
        // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
3391
        // COURSE_TIMELINE_PAST
3392
        // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
3393
        //
3394
        // NOTE: The offset applies to the unfiltered full set of courses before the classification
3395
        // filtering is done.
3396
        // E.g. In our example if an offset of 2 is given then it would mean the first
3397
        // two courses (afuture, ainprogress) are ignored.
3398
        return [
3399
            'empty set' => [
3400
                'coursedata' => [],
3401
                'classification' => 'future',
3402
                'limit' => 2,
3403
                'offset' => 0,
3404
                'sort' => 'shortname ASC',
3405
                'expectedcourses' => [],
3406
                'expectednextoffset' => 0,
3407
            ],
3408
            // COURSE_TIMELINE_FUTURE.
3409
            'future not limit no offset' => [
3410
                'coursedata' => $coursedata,
3411
                'classification' => 'future',
3412
                'limit' => 0,
3413
                'offset' => 0,
3414
                'sort' => 'shortname ASC',
3415
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3416
                'expectednextoffset' => 15,
3417
            ],
3418
            'future no offset' => [
3419
                'coursedata' => $coursedata,
3420
                'classification' => 'future',
3421
                'limit' => 2,
3422
                'offset' => 0,
3423
                'sort' => 'shortname ASC',
3424
                'expectedcourses' => ['afuture', 'bfuture'],
3425
                'expectednextoffset' => 4,
3426
            ],
3427
            'future offset' => [
3428
                'coursedata' => $coursedata,
3429
                'classification' => 'future',
3430
                'limit' => 2,
3431
                'offset' => 2,
3432
                'sort' => 'shortname ASC',
3433
                'expectedcourses' => ['bfuture', 'cfuture'],
3434
                'expectednextoffset' => 7,
3435
            ],
3436
            'future exact limit' => [
3437
                'coursedata' => $coursedata,
3438
                'classification' => 'future',
3439
                'limit' => 5,
3440
                'offset' => 0,
3441
                'sort' => 'shortname ASC',
3442
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3443
                'expectednextoffset' => 13,
3444
            ],
3445
            'future limit less results' => [
3446
                'coursedata' => $coursedata,
3447
                'classification' => 'future',
3448
                'limit' => 10,
3449
                'offset' => 0,
3450
                'sort' => 'shortname ASC',
3451
                'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
3452
                'expectednextoffset' => 15,
3453
            ],
3454
            'future limit less results with offset' => [
3455
                'coursedata' => $coursedata,
3456
                'classification' => 'future',
3457
                'limit' => 10,
3458
                'offset' => 5,
3459
                'sort' => 'shortname ASC',
3460
                'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
3461
                'expectednextoffset' => 15,
3462
            ],
3463
            'all no limit or offset' => [
3464
                'coursedata' => $coursedata,
3465
                'classification' => 'all',
3466
                'limit' => 0,
3467
                'offset' => 0,
3468
                'sort' => 'shortname ASC',
3469
                'expectedcourses' => [
3470
                    'afuture',
3471
                    'ainprogress',
3472
                    'apast',
3473
                    'bfuture',
3474
                    'binprogress',
3475
                    'bpast',
3476
                    'cfuture',
3477
                    'cinprogress',
3478
                    'cpast',
3479
                    'dfuture',
3480
                    'dinprogress',
3481
                    'dpast',
3482
                    'efuture',
3483
                    'einprogress',
3484
                    'epast'
3485
                ],
3486
                'expectednextoffset' => 15,
3487
            ],
3488
            'all limit no offset' => [
3489
                'coursedata' => $coursedata,
3490
                'classification' => 'all',
3491
                'limit' => 5,
3492
                'offset' => 0,
3493
                'sort' => 'shortname ASC',
3494
                'expectedcourses' => [
3495
                    'afuture',
3496
                    'ainprogress',
3497
                    'apast',
3498
                    'bfuture',
3499
                    'binprogress'
3500
                ],
3501
                'expectednextoffset' => 5,
3502
            ],
3503
            'all limit and offset' => [
3504
                'coursedata' => $coursedata,
3505
                'classification' => 'all',
3506
                'limit' => 5,
3507
                'offset' => 5,
3508
                'sort' => 'shortname ASC',
3509
                'expectedcourses' => [
3510
                    'bpast',
3511
                    'cfuture',
3512
                    'cinprogress',
3513
                    'cpast',
3514
                    'dfuture'
3515
                ],
3516
                'expectednextoffset' => 10,
3517
            ],
3518
            'all offset past result set' => [
3519
                'coursedata' => $coursedata,
3520
                'classification' => 'all',
3521
                'limit' => 5,
3522
                'offset' => 50,
3523
                'sort' => 'shortname ASC',
3524
                'expectedcourses' => [],
3525
                'expectednextoffset' => 50,
3526
            ],
3527
            'all limit and offset with sort ul.timeaccess desc' => [
3528
                'coursedata' => $coursedata,
3529
                'classification' => 'inprogress',
3530
                'limit' => 0,
3531
                'offset' => 0,
3532
                'sort' => 'ul.timeaccess desc',
3533
                'expectedcourses' => [
3534
                    'ainprogress',
3535
                    'binprogress',
3536
                    'cinprogress',
3537
                    'dinprogress',
3538
                    'einprogress'
3539
                ],
3540
                'expectednextoffset' => 15,
3541
            ],
3542
            'all limit and offset with sort sql injection for sort or 1==1' => [
3543
                'coursedata' => $coursedata,
3544
                'classification' => 'all',
3545
                'limit' => 5,
3546
                'offset' => 5,
3547
                'sort' => 'ul.timeaccess desc or 1==1',
3548
                'expectedcourses' => [],
3549
                'expectednextoffset' => 0,
3550
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3551
            ],
3552
            'all limit and offset with sql injection of sort a custom one' => [
3553
                'coursedata' => $coursedata,
3554
                'classification' => 'all',
3555
                'limit' => 5,
3556
                'offset' => 5,
3557
                'sort' => "ul.timeaccess LIMIT 1--",
3558
                'expectedcourses' => [],
3559
                'expectednextoffset' => 0,
3560
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3561
            ],
3562
            'all limit and offset with wrong sort direction' => [
3563
                'coursedata' => $coursedata,
3564
                'classification' => 'all',
3565
                'limit' => 5,
3566
                'offset' => 5,
3567
                'sort' => "ul.timeaccess.foo ascd",
3568
                'expectedcourses' => [],
3569
                'expectednextoffset' => 0,
3570
                'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()',
3571
            ],
3572
            'all limit and offset with wrong sort param' => [
3573
                'coursedata' => $coursedata,
3574
                'classification' => 'all',
3575
                'limit' => 5,
3576
                'offset' => 5,
3577
                'sort' => "foobar",
3578
                'expectedcourses' => [],
3579
                'expectednextoffset' => 0,
3580
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3581
            ],
3582
            'all limit and offset with wrong field separator' => [
3583
                'coursedata' => $coursedata,
3584
                'classification' => 'all',
3585
                'limit' => 5,
3586
                'offset' => 5,
3587
                'sort' => "ul.timeaccess.foo",
3588
                'expectedcourses' => [],
3589
                'expectednextoffset' => 0,
3590
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3591
            ],
3592
            'all limit and offset with wrong field separator #' => [
3593
                'coursedata' => $coursedata,
3594
                'classification' => 'all',
3595
                'limit' => 5,
3596
                'offset' => 5,
3597
                'sort' => "ul#timeaccess",
3598
                'expectedcourses' => [],
3599
                'expectednextoffset' => 0,
3600
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3601
            ],
3602
            'all limit and offset with wrong field separator $' => [
3603
                'coursedata' => $coursedata,
3604
                'classification' => 'all',
3605
                'limit' => 5,
3606
                'offset' => 5,
3607
                'sort' => 'ul$timeaccess',
3608
                'expectedcourses' => [],
3609
                'expectednextoffset' => 0,
3610
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3611
            ],
3612
            'all limit and offset with wrong field name' => [
3613
                'coursedata' => $coursedata,
3614
                'classification' => 'all',
3615
                'limit' => 5,
3616
                'offset' => 5,
3617
                'sort' => 'timeaccess123',
3618
                'expectedcourses' => [],
3619
                'expectednextoffset' => 0,
3620
                'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()',
3621
            ],
3622
            'all limit and offset with no sort direction for ul' => [
3623
                'coursedata' => $coursedata,
3624
                'classification' => 'inprogress',
3625
                'limit' => 0,
3626
                'offset' => 0,
3627
                'sort' => "ul.timeaccess",
3628
                'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3629
                'expectednextoffset' => 15,
3630
            ],
3631
            'all limit and offset with valid field name and no prefix, test for ul' => [
3632
                'coursedata' => $coursedata,
3633
                'classification' => 'inprogress',
3634
                'limit' => 0,
3635
                'offset' => 0,
3636
                'sort' => "timeaccess",
3637
                'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3638
                'expectednextoffset' => 15,
3639
            ],
3640
            'all limit and offset with valid field name and no prefix' => [
3641
                'coursedata' => $coursedata,
3642
                'classification' => 'all',
3643
                'limit' => 5,
3644
                'offset' => 5,
3645
                'sort' => "fullname",
3646
                'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3647
                'expectednextoffset' => 10,
3648
            ],
3649
            'all limit and offset with valid field name and no prefix and with sort direction' => [
3650
                'coursedata' => $coursedata,
3651
                'classification' => 'all',
3652
                'limit' => 5,
3653
                'offset' => 5,
3654
                'sort' => "fullname desc",
3655
                'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'],
3656
                'expectednextoffset' => 10,
3657
            ],
3658
            'Search courses for courses containing bfut' => [
3659
                'coursedata' => $coursedata,
3660
                'classification' => 'search',
3661
                'limit' => 0,
3662
                'offset' => 0,
3663
                'sort' => null,
3664
                'expectedcourses' => ['bfuture'],
3665
                'expectednextoffset' => 1,
3666
                'expectedexception' => null,
3667
                'searchvalue' => 'bfut',
3668
            ],
3669
            'Search courses for courses containing inp' => [
3670
                'coursedata' => $coursedata,
3671
                'classification' => 'search',
3672
                'limit' => 0,
3673
                'offset' => 0,
3674
                'sort' => null,
3675
                'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'],
3676
                'expectednextoffset' => 5,
3677
                'expectedexception' => null,
3678
                'searchvalue' => 'inp',
3679
            ],
3680
            'Search courses for courses containing fail' => [
3681
                'coursedata' => $coursedata,
3682
                'classification' => 'search',
3683
                'limit' => 0,
3684
                'offset' => 0,
3685
                'sort' => null,
3686
                'expectedcourses' => [],
3687
                'expectednextoffset' => 0,
3688
                'expectedexception' => null,
3689
                'searchvalue' => 'fail',
3690
            ],
3691
            'Search courses for courses containing !`~[]C' => [
3692
                'coursedata' => $coursedata,
3693
                'classification' => 'search',
3694
                'limit' => 0,
3695
                'offset' => 0,
3696
                'sort' => null,
3697
                'expectedcourses' => [],
3698
                'expectednextoffset' => 0,
3699
                'expectedexception' => null,
3700
                'searchvalue' => '!`~[]C',
3701
            ],
3702
        ];
3703
    }
3704
 
3705
    /**
3706
     * Test the get_enrolled_courses_by_timeline_classification function.
3707
     *
3708
     * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases()
3709
     * @param array $coursedata Courses to create
3710
     * @param string $classification Timeline classification
3711
     * @param int $limit Maximum number of results
3712
     * @param int $offset Offset the unfiltered courses result set by this amount
3713
     * @param string $sort sort the courses
3714
     * @param array $expectedcourses Expected courses in result
3715
     * @param int $expectednextoffset Expected next offset value in result
3716
     * @param string|null $expectedexception Expected exception string
3717
     * @param string|null $searchvalue If we are searching, what do we need to look for?
3718
     */
3719
    public function test_get_enrolled_courses_by_timeline_classification(
3720
        $coursedata,
3721
        $classification,
3722
        $limit,
3723
        $offset,
3724
        $sort,
3725
        $expectedcourses,
3726
        $expectednextoffset,
3727
        $expectedexception = null,
3728
        $searchvalue = null
11 efrain 3729
    ): void {
1 efrain 3730
        $this->resetAfterTest();
3731
        $generator = $this->getDataGenerator();
3732
 
3733
        $courses = array_map(function($coursedata) use ($generator) {
3734
            return $generator->create_course($coursedata);
3735
        }, $coursedata);
3736
 
3737
        $student = $generator->create_user();
3738
 
3739
        foreach ($courses as $course) {
3740
            $generator->enrol_user($student->id, $course->id, 'student');
3741
        }
3742
 
3743
        $this->setUser($student);
3744
 
3745
        if (isset($expectedexception)) {
3746
            $this->expectException('coding_exception');
3747
            $this->expectExceptionMessage($expectedexception);
3748
        }
3749
 
3750
        // NOTE: The offset applies to the unfiltered full set of courses before the classification
3751
        // filtering is done.
3752
        // E.g. In our example if an offset of 2 is given then it would mean the first
3753
        // two courses (afuture, ainprogress) are ignored.
3754
        $result = core_course_external::get_enrolled_courses_by_timeline_classification(
3755
            $classification,
3756
            $limit,
3757
            $offset,
3758
            $sort,
3759
            null,
3760
            null,
3761
            $searchvalue
3762
        );
3763
        $result = external_api::clean_returnvalue(
3764
            core_course_external::get_enrolled_courses_by_timeline_classification_returns(),
3765
            $result
3766
        );
3767
 
3768
        $actual = array_map(function($course) {
3769
            return $course['shortname'];
3770
        }, $result['courses']);
3771
 
3772
        $this->assertEqualsCanonicalizing($expectedcourses, $actual);
3773
        $this->assertEquals($expectednextoffset, $result['nextoffset']);
3774
    }
3775
 
3776
    /**
3777
     * Test the get_recent_courses function.
3778
     */
11 efrain 3779
    public function test_get_recent_courses(): void {
1 efrain 3780
        global $USER, $DB;
3781
 
3782
        $this->resetAfterTest();
3783
        $generator = $this->getDataGenerator();
3784
 
3785
        set_config('hiddenuserfields', 'lastaccess');
3786
 
3787
        $courses = array();
3788
        for ($i = 1; $i < 12; $i++) {
3789
            $courses[]  = $generator->create_course();
3790
        };
3791
 
3792
        $student = $generator->create_user();
3793
        $teacher = $generator->create_user();
3794
 
3795
        foreach ($courses as $course) {
3796
            $generator->enrol_user($student->id, $course->id, 'student');
3797
        }
3798
 
3799
        $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher');
3800
 
3801
        $this->setUser($student);
3802
 
3803
        $result = core_course_external::get_recent_courses($USER->id);
3804
 
3805
        // No course accessed.
3806
        $this->assertCount(0, $result);
3807
 
3808
        foreach ($courses as $course) {
3809
            core_course_external::view_course($course->id);
3810
        }
3811
 
3812
        // Every course accessed.
3813
        $result = core_course_external::get_recent_courses($USER->id);
3814
        $this->assertCount( 11, $result);
3815
 
3816
        // Every course accessed, result limited to 10 courses.
3817
        $result = core_course_external::get_recent_courses($USER->id, 10);
3818
        $this->assertCount(10, $result);
3819
 
3820
        $guestcourse = $generator->create_course(
3821
                (object)array('shortname' => 'guestcourse',
3822
                'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
3823
                'enrol_guest_password_0' => ''));
3824
        core_course_external::view_course($guestcourse->id);
3825
 
3826
        // Every course accessed, even the not enrolled one.
3827
        $result = core_course_external::get_recent_courses($USER->id);
3828
        $this->assertCount(12, $result);
3829
 
3830
        // Offset 5, return 7 out of 12.
3831
        $result = core_course_external::get_recent_courses($USER->id, 0, 5);
3832
        $this->assertCount(7, $result);
3833
 
3834
        // Offset 5 and limit 3, return 3 out of 12.
3835
        $result = core_course_external::get_recent_courses($USER->id, 3, 5);
3836
        $this->assertCount(3, $result);
3837
 
3838
        // Sorted by course id ASC.
3839
        $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC');
3840
        $this->assertEquals($courses[0]->id, array_shift($result)->id);
3841
 
3842
        // Sorted by course id DESC.
3843
        $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC');
3844
        $this->assertEquals($guestcourse->id, array_shift($result)->id);
3845
 
3846
        // If last access is hidden, only get the courses where has viewhiddenuserfields capability.
3847
        $this->setUser($teacher);
3848
        $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
3849
        $usercontext = context_user::instance($student->id);
3850
        $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid);
3851
 
3852
        // Sorted by course id DESC.
3853
        $result = core_course_external::get_recent_courses($student->id);
3854
        $this->assertCount(1, $result);
3855
        $this->assertEquals($courses[0]->id, array_shift($result)->id);
3856
    }
3857
 
3858
    /**
3859
     * Test get enrolled users by cmid function.
3860
     */
11 efrain 3861
    public function test_get_enrolled_users_by_cmid(): void {
1 efrain 3862
        global $PAGE;
3863
        $this->resetAfterTest(true);
3864
 
3865
        $user1 = self::getDataGenerator()->create_user();
3866
        $user2 = self::getDataGenerator()->create_user();
3867
        $user3 = self::getDataGenerator()->create_user();
3868
 
3869
        $user1picture = new user_picture($user1);
3870
        $user1picture->size = 1;
3871
        $user1->profileimage = $user1picture->get_url($PAGE)->out(false);
3872
 
3873
        $user2picture = new user_picture($user2);
3874
        $user2picture->size = 1;
3875
        $user2->profileimage = $user2picture->get_url($PAGE)->out(false);
3876
 
3877
        $user3picture = new user_picture($user3);
3878
        $user3picture->size = 1;
3879
        $user3->profileimage = $user3picture->get_url($PAGE)->out(false);
3880
 
3881
        // Set the first created user to the test user.
3882
        self::setUser($user1);
3883
 
3884
        // Create course to add the module.
3885
        $course1 = self::getDataGenerator()->create_course();
3886
 
3887
        // Forum with tracking off.
3888
        $record = new stdClass();
3889
        $record->course = $course1->id;
3890
        $forum1 = self::getDataGenerator()->create_module('forum', $record);
3891
 
3892
        // Following lines enrol and assign default role id to the users.
3893
        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
3894
        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
3895
        // Enrol a suspended user in the course.
3896
        $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED);
3897
 
3898
        // Create what we expect to be returned when querying the course module.
3899
        $expectedusers = array(
3900
            'users' => array(),
3901
            'warnings' => array(),
3902
        );
3903
 
3904
        $expectedusers['users'][0] = [
3905
            'id' => $user1->id,
3906
            'fullname' => fullname($user1),
3907
            'firstname' => $user1->firstname,
3908
            'lastname' => $user1->lastname,
3909
            'profileimage' => $user1->profileimage,
3910
        ];
3911
        $expectedusers['users'][1] = [
3912
            'id' => $user2->id,
3913
            'fullname' => fullname($user2),
3914
            'firstname' => $user2->firstname,
3915
            'lastname' => $user2->lastname,
3916
            'profileimage' => $user2->profileimage,
3917
        ];
3918
        $expectedusers['users'][2] = [
3919
            'id' => $user3->id,
3920
            'fullname' => fullname($user3),
3921
            'firstname' => $user3->firstname,
3922
            'lastname' => $user3->lastname,
3923
            'profileimage' => $user3->profileimage,
3924
        ];
3925
 
3926
        // Test getting the users in a given context.
3927
        $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
3928
        $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3929
 
3930
        $this->assertEquals(3, count($users['users']));
3931
        $this->assertEquals($expectedusers, $users);
3932
 
3933
        // Test getting only the active users in a given context.
3934
        $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid, 0, true);
3935
        $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
3936
 
3937
        $expectedusers['users'] = [
3938
            [
3939
                'id' => $user1->id,
3940
                'fullname' => fullname($user1),
3941
                'firstname' => $user1->firstname,
3942
                'lastname' => $user1->lastname,
3943
                'profileimage' => $user1->profileimage,
3944
            ],
3945
            [
3946
                'id' => $user2->id,
3947
                'fullname' => fullname($user2),
3948
                'firstname' => $user2->firstname,
3949
                'lastname' => $user2->lastname,
3950
                'profileimage' => $user2->profileimage,
3951
            ]
3952
        ];
3953
 
3954
        $this->assertEquals(2, count($users['users']));
3955
        $this->assertEquals($expectedusers, $users);
3956
    }
3957
 
3958
    /**
3959
     * Verify that content items can be added to user favourites.
3960
     */
11 efrain 3961
    public function test_add_content_item_to_user_favourites(): void {
1 efrain 3962
        $this->resetAfterTest();
3963
 
3964
        $course = $this->getDataGenerator()->create_course();
3965
        $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
3966
        $this->setUser($user);
3967
 
3968
        // Using the internal API, confirm that no items are set as favourites for the user.
3969
        $contentitemservice = new \core_course\local\service\content_item_service(
3970
            new \core_course\local\repository\content_item_readonly_repository()
3971
        );
3972
        $contentitems = $contentitemservice->get_all_content_items($user);
3973
        $favourited = array_filter($contentitems, function($contentitem) {
3974
            return $contentitem->favourite == true;
3975
        });
3976
        $this->assertCount(0, $favourited);
3977
 
3978
        // Using the external API, favourite a content item for the user.
3979
        $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
3980
        $contentitem = core_course_external::add_content_item_to_user_favourites('mod_assign', $assign->id, $user->id);
3981
        $contentitem = external_api::clean_returnvalue(core_course_external::add_content_item_to_user_favourites_returns(),
3982
            $contentitem);
3983
 
3984
        // Verify the returned item is a favourite.
3985
        $this->assertTrue($contentitem['favourite']);
3986
 
3987
        // Using the internal API, confirm we see a single favourite item.
3988
        $contentitems = $contentitemservice->get_all_content_items($user);
3989
        $favourited = array_values(array_filter($contentitems, function($contentitem) {
3990
            return $contentitem->favourite == true;
3991
        }));
3992
        $this->assertCount(1, $favourited);
3993
        $this->assertEquals('assign', $favourited[0]->name);
3994
    }
3995
 
3996
    /**
3997
     * Verify that content items can be removed from user favourites.
3998
     */
11 efrain 3999
    public function test_remove_content_item_from_user_favourites(): void {
1 efrain 4000
        $this->resetAfterTest();
4001
 
4002
        $course = $this->getDataGenerator()->create_course();
4003
        $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
4004
        $this->setUser($user);
4005
 
4006
        // Using the internal API, set a favourite for the user.
4007
        $contentitemservice = new \core_course\local\service\content_item_service(
4008
            new \core_course\local\repository\content_item_readonly_repository()
4009
        );
4010
        $contentitems = $contentitemservice->get_all_content_items($user);
4011
        $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))];
4012
        $contentitemservice->add_to_user_favourites($user, $assign->componentname, $assign->id);
4013
 
4014
        $contentitems = $contentitemservice->get_all_content_items($user);
4015
        $favourited = array_filter($contentitems, function($contentitem) {
4016
            return $contentitem->favourite == true;
4017
        });
4018
        $this->assertCount(1, $favourited);
4019
 
4020
        // Now, verify the external API can remove the favourite.
4021
        $contentitem = core_course_external::remove_content_item_from_user_favourites('mod_assign', $assign->id);
4022
        $contentitem = external_api::clean_returnvalue(core_course_external::remove_content_item_from_user_favourites_returns(),
4023
            $contentitem);
4024
 
4025
        // Verify the returned item is a favourite.
4026
        $this->assertFalse($contentitem['favourite']);
4027
 
4028
        // Using the internal API, confirm we see no favourite items.
4029
        $contentitems = $contentitemservice->get_all_content_items($user);
4030
        $favourited = array_filter($contentitems, function($contentitem) {
4031
            return $contentitem->favourite == true;
4032
        });
4033
        $this->assertCount(0, $favourited);
4034
    }
4035
 
4036
    /**
4037
     * Test the web service returning course content items for inclusion in activity choosers, etc.
4038
     */
11 efrain 4039
    public function test_get_course_content_items(): void {
1 efrain 4040
        $this->resetAfterTest();
4041
 
4042
        $course  = self::getDataGenerator()->create_course();
4043
        $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
4044
 
4045
        // Fetch available content items as the editing teacher.
4046
        $this->setUser($user);
4047
        $result = core_course_external::get_course_content_items($course->id);
4048
        $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
4049
 
4050
        $contentitemservice = new \core_course\local\service\content_item_service(
4051
            new \core_course\local\repository\content_item_readonly_repository()
4052
        );
4053
 
4054
        // Check if the webservice returns exactly what the service defines, albeit in array form.
4055
        $serviceitemsasarray = array_map(function($item) {
4056
            return (array) $item;
4057
        }, $contentitemservice->get_content_items_for_user_in_course($user, $course));
4058
 
4059
        $this->assertEquals($serviceitemsasarray, $result['content_items']);
4060
    }
4061
 
4062
    /**
4063
     * Test the web service returning course content items, specifically in case where the user can't manage activities.
4064
     */
11 efrain 4065
    public function test_get_course_content_items_no_permission_to_manage(): void {
1 efrain 4066
        $this->resetAfterTest();
4067
 
4068
        $course  = self::getDataGenerator()->create_course();
4069
        $user = self::getDataGenerator()->create_and_enrol($course, 'student');
4070
 
4071
        // Fetch available content items as a student, who won't have the permission to manage activities.
4072
        $this->setUser($user);
4073
        $result = core_course_external::get_course_content_items($course->id);
4074
        $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result);
4075
 
4076
        $this->assertEmpty($result['content_items']);
4077
    }
4078
 
4079
    /**
4080
     * Test toggling the recommendation of an activity.
4081
     */
11 efrain 4082
    public function test_toggle_activity_recommendation(): void {
1 efrain 4083
        global $CFG;
4084
 
4085
        $this->resetAfterTest();
4086
 
4087
        $context = context_system::instance();
4088
        $usercontext = context_user::instance($CFG->siteguest);
4089
        $component = 'core_course';
4090
        $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4091
 
4092
        $areaname = 'test_core';
4093
        $areaid = 3;
4094
 
4095
        // Test we have the favourite.
4096
        $this->setAdminUser();
4097
        $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
4098
        $this->assertTrue($favouritefactory->favourite_exists($component,
4099
                \core_course\local\service\content_item_service::RECOMMENDATION_PREFIX . $areaname, $areaid, $context));
4100
        $this->assertTrue($result['status']);
4101
        // Test that it is now gone.
4102
        $result = core_course_external::toggle_activity_recommendation($areaname, $areaid);
4103
        $this->assertFalse($favouritefactory->favourite_exists($component, $areaname, $areaid, $context));
4104
        $this->assertFalse($result['status']);
4105
    }
4106
 
4107
}