Proyectos de Subversion Moodle

Rev

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

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core_question;
18
 
19
use core_external\restricted_context_exception;
20
use core_question_external;
21
use externallib_advanced_testcase;
22
 
23
defined('MOODLE_INTERNAL') || die();
24
 
25
global $CFG;
26
 
27
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
28
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
29
 
30
/**
31
 * Question external functions tests
32
 *
33
 * @package    core_question
34
 * @covers     \core_question_external
35
 * @category   external
36
 * @copyright  2016 Pau Ferrer <pau@moodle.com>
37
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38
 * @since      Moodle 3.1
39
 */
1441 ariadna 40
final class externallib_test extends externallib_advanced_testcase {
1 efrain 41
 
42
    /** @var \stdClass course record. */
43
    protected $course;
44
 
45
    /** @var \stdClass user record. */
46
    protected $student;
47
 
48
    /** @var \stdClass user role record. */
49
    protected $studentrole;
50
 
51
    /**
52
     * Set up for every test
53
     */
54
    public function setUp(): void {
55
        global $DB;
1441 ariadna 56
        parent::setUp();
1 efrain 57
        $this->resetAfterTest();
58
        $this->setAdminUser();
59
 
60
        // Setup test data.
61
        $this->course = $this->getDataGenerator()->create_course();
62
 
63
        // Create users.
64
        $this->student = self::getDataGenerator()->create_user();
65
 
66
        // Users enrolments.
67
        $this->studentrole = $DB->get_record('role', ['shortname' => 'student']);
68
        $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
69
    }
70
 
71
    /**
72
     * Test update question flag
73
     */
11 efrain 74
    public function test_core_question_update_flag(): void {
1 efrain 75
 
76
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
77
 
78
        // Create a question category.
79
        $cat = $questiongenerator->create_question_category();
80
 
81
        $quba = \question_engine::make_questions_usage_by_activity('core_question_update_flag', \context_system::instance());
82
        $quba->set_preferred_behaviour('deferredfeedback');
83
        $questiondata = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
84
        $question = \question_bank::load_question($questiondata->id);
85
        $slot = $quba->add_question($question);
86
        $qa = $quba->get_question_attempt($slot);
87
 
88
        self::setUser($this->student);
89
 
90
        $quba->start_all_questions();
91
        \question_engine::save_questions_usage_by_activity($quba);
92
 
93
        $qubaid = $quba->get_id();
94
        $questionid = $question->id;
95
        $qaid = $qa->get_database_id();
96
        $checksum = md5($qubaid . "_" . $this->student->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
97
 
98
        $flag = core_question_external::update_flag($qubaid, $questionid, $qaid, $slot, $checksum, true);
99
        $this->assertTrue($flag['status']);
100
 
101
        // Test invalid checksum.
102
        try {
103
            // Using random_string to force failing.
104
            $checksum = md5($qubaid . "_" . random_string(11) . "_" . $questionid . "_" . $qaid . "_" . $slot);
105
 
106
            core_question_external::update_flag($qubaid, $questionid, $qaid, $slot, $checksum, true);
107
            $this->fail('Exception expected due to invalid checksum.');
108
        } catch (\moodle_exception $e) {
109
            $this->assertEquals('errorsavingflags', $e->errorcode);
110
        }
111
    }
112
 
113
    /**
114
     * Data provider for the get_random_question_summaries test.
115
     */
1441 ariadna 116
    public static function get_random_question_summaries_test_cases(): array {
1 efrain 117
        return [
118
            'empty category' => [
119
                'categoryindex' => 'emptycat',
120
                'includesubcategories' => false,
121
                'usetagnames' => [],
122
                'expectedquestionindexes' => []
123
            ],
124
            'single category' => [
125
                'categoryindex' => 'cat1',
126
                'includesubcategories' => false,
127
                'usetagnames' => [],
128
                'expectedquestionindexes' => ['cat1q1', 'cat1q2']
129
            ],
130
            'include sub category' => [
131
                'categoryindex' => 'cat1',
132
                'includesubcategories' => true,
133
                'usetagnames' => [],
134
                'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2']
135
            ],
136
            'single category with tags' => [
137
                'categoryindex' => 'cat1',
138
                'includesubcategories' => false,
139
                'usetagnames' => ['cat1'],
140
                'expectedquestionindexes' => ['cat1q1']
141
            ],
142
            'include sub category with tag on parent' => [
143
                'categoryindex' => 'cat1',
144
                'includesubcategories' => true,
145
                'usetagnames' => ['cat1'],
146
                'expectedquestionindexes' => ['cat1q1']
147
            ],
148
            'include sub category with tag on sub' => [
149
                'categoryindex' => 'cat1',
150
                'includesubcategories' => true,
151
                'usetagnames' => ['subcat'],
152
                'expectedquestionindexes' => ['subcatq1']
153
            ],
154
            'include sub category with same tag on parent and sub' => [
155
                'categoryindex' => 'cat1',
156
                'includesubcategories' => true,
157
                'usetagnames' => ['foo'],
158
                'expectedquestionindexes' => ['cat1q1', 'subcatq1']
159
            ],
160
            'include sub category with tag not matching' => [
161
                'categoryindex' => 'cat1',
162
                'includesubcategories' => true,
163
                'usetagnames' => ['cat1', 'cat2'],
164
                'expectedquestionindexes' => []
165
            ]
166
        ];
167
    }
168
 
169
    /**
170
     * Test the get_random_question_summaries function with various parameter combinations.
171
     *
172
     * This function creates a data set as follows:
173
     *      Category: cat1
174
     *          Question: cat1q1
175
     *              Tags: 'cat1', 'foo'
176
     *          Question: cat1q2
177
     *      Category: cat2
178
     *          Question: cat2q1
179
     *              Tags: 'cat2', 'foo'
180
     *          Question: cat2q2
181
     *      Category: subcat
182
     *          Question: subcatq1
183
     *              Tags: 'subcat', 'foo'
184
     *          Question: subcatq2
185
     *          Parent: cat1
186
     *      Category: emptycat
187
     *
1441 ariadna 188
     * @dataProvider get_random_question_summaries_test_cases
1 efrain 189
     * @param string $categoryindex The named index for the category to use
190
     * @param bool $includesubcategories If the search should include subcategories
191
     * @param string[] $usetagnames The tag names to include in the search
192
     * @param string[] $expectedquestionindexes The questions expected in the result
193
     */
194
    public function test_get_random_question_summaries_variations(
195
        $categoryindex,
196
        $includesubcategories,
197
        $usetagnames,
198
        $expectedquestionindexes
11 efrain 199
    ): void {
1 efrain 200
        $this->resetAfterTest();
201
 
202
        $context = \context_system::instance();
203
        $categories = [];
204
        $questions = [];
205
        $tagnames = [
206
            'cat1',
207
            'cat2',
208
            'subcat',
209
            'foo'
210
        ];
211
        $collid = \core_tag_collection::get_default();
212
        $tags = \core_tag_tag::create_if_missing($collid, $tagnames);
213
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
214
 
215
        // First category and questions.
216
        list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat1', 'foo']);
217
        $categories['cat1'] = $category;
218
        $questions['cat1q1'] = $categoryquestions[0];
219
        $questions['cat1q2'] = $categoryquestions[1];
220
        // Second category and questions.
221
        list($category, $categoryquestions) = $this->create_category_and_questions(2, ['cat2', 'foo']);
222
        $categories['cat2'] = $category;
223
        $questions['cat2q1'] = $categoryquestions[0];
224
        $questions['cat2q2'] = $categoryquestions[1];
225
        // Sub category and questions.
226
        list($category, $categoryquestions) = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
227
        $categories['subcat'] = $category;
228
        $questions['subcatq1'] = $categoryquestions[0];
229
        $questions['subcatq2'] = $categoryquestions[1];
230
        // Empty category.
231
        list($category, $categoryquestions) = $this->create_category_and_questions(0);
232
        $categories['emptycat'] = $category;
233
 
234
        // Generate the arguments for the get_questions function.
235
        $category = $categories[$categoryindex];
236
        $tagids = array_map(function($tagname) use ($tags) {
237
            return $tags[$tagname]->id;
238
        }, $usetagnames);
239
 
240
        $result = core_question_external::get_random_question_summaries($category->id, $includesubcategories, $tagids, $context->id);
241
        $resultquestions = $result['questions'];
242
        $resulttotalcount = $result['totalcount'];
243
        // Generate the expected question set.
244
        $expectedquestions = array_map(function($index) use ($questions) {
245
            return $questions[$index];
246
        }, $expectedquestionindexes);
247
 
248
        // Ensure the resultquestions matches what was expected.
249
        $this->assertCount(count($expectedquestions), $resultquestions);
250
        $this->assertEquals(count($expectedquestions), $resulttotalcount);
251
        foreach ($expectedquestions as $question) {
252
            $this->assertEquals($resultquestions[$question->id]->id, $question->id);
253
            $this->assertEquals($resultquestions[$question->id]->category, $question->category);
254
        }
255
    }
256
 
257
    /**
258
     * get_random_question_summaries should throw an invalid_parameter_exception if not
259
     * given an integer for the category id.
260
     */
11 efrain 261
    public function test_get_random_question_summaries_invalid_category_id_param(): void {
1 efrain 262
        $this->resetAfterTest();
263
 
264
        $context = \context_system::instance();
265
        $this->expectException('\invalid_parameter_exception');
266
        core_question_external::get_random_question_summaries('invalid value', false, [], $context->id);
267
    }
268
 
269
    /**
270
     * get_random_question_summaries should throw an invalid_parameter_exception if not
271
     * given a boolean for the $includesubcategories parameter.
272
     */
11 efrain 273
    public function test_get_random_question_summaries_invalid_includesubcategories_param(): void {
1 efrain 274
        $this->resetAfterTest();
275
 
276
        $context = \context_system::instance();
277
        $this->expectException('\invalid_parameter_exception');
278
        core_question_external::get_random_question_summaries(1, 'invalid value', [], $context->id);
279
    }
280
 
281
    /**
282
     * get_random_question_summaries should throw an invalid_parameter_exception if not
283
     * given an array of integers for the tag ids parameter.
284
     */
11 efrain 285
    public function test_get_random_question_summaries_invalid_tagids_param(): void {
1 efrain 286
        $this->resetAfterTest();
287
 
288
        $context = \context_system::instance();
289
        $this->expectException('\invalid_parameter_exception');
290
        core_question_external::get_random_question_summaries(1, false, ['invalid', 'values'], $context->id);
291
    }
292
 
293
    /**
294
     * get_random_question_summaries should throw an invalid_parameter_exception if not
295
     * given a context.
296
     */
11 efrain 297
    public function test_get_random_question_summaries_invalid_context(): void {
1 efrain 298
        $this->resetAfterTest();
299
 
300
        $this->expectException('\invalid_parameter_exception');
301
        core_question_external::get_random_question_summaries(1, false, [1, 2], 'context');
302
    }
303
 
304
    /**
305
     * get_random_question_summaries should throw an restricted_context_exception
306
     * if the given context is outside of the set of restricted contexts the user
307
     * is allowed to call external functions in.
308
     */
11 efrain 309
    public function test_get_random_question_summaries_restricted_context(): void {
1 efrain 310
        $this->resetAfterTest();
311
 
312
        $course = $this->getDataGenerator()->create_course();
313
        $coursecontext = \context_course::instance($course->id);
314
        $systemcontext = \context_system::instance();
315
        // Restrict access to external functions for the logged in user to only
316
        // the course we just created. External functions should not be allowed
317
        // to execute in any contexts above the course context.
318
        core_question_external::set_context_restriction($coursecontext);
319
 
320
        // An exception should be thrown when we try to execute at the system context
321
        // since we're restricted to the course context.
322
        try {
323
            // Do this in a try/catch statement to allow the context restriction
324
            // to be reset afterwards.
325
            core_question_external::get_random_question_summaries(1, false, [], $systemcontext->id);
326
        } catch (\Exception $e) {
327
            $this->assertInstanceOf(restricted_context_exception::class, $e);
328
        }
329
        // Reset the restriction so that other tests don't fail aftwards.
330
        core_question_external::set_context_restriction($systemcontext);
331
    }
332
 
333
    /**
334
     * get_random_question_summaries should return a question that is formatted correctly.
335
     */
11 efrain 336
    public function test_get_random_question_summaries_formats_returned_questions(): void {
1 efrain 337
        $this->resetAfterTest();
338
 
339
        list($category, $questions) = $this->create_category_and_questions(1);
340
        $context = \context_system::instance();
341
        $question = $questions[0];
342
        $expected = (object) [
343
            'id' => $question->id,
344
            'category' => $question->category,
345
            'parent' => $question->parent,
346
            'name' => $question->name,
347
            'qtype' => $question->qtype
348
        ];
349
 
350
        $result = core_question_external::get_random_question_summaries($category->id, false, [], $context->id);
351
        $actual = $result['questions'][$question->id];
352
 
353
        $this->assertEquals($expected->id, $actual->id);
354
        $this->assertEquals($expected->category, $actual->category);
355
        $this->assertEquals($expected->parent, $actual->parent);
356
        $this->assertEquals($expected->name, $actual->name);
357
        $this->assertEquals($expected->qtype, $actual->qtype);
358
        // These values are added by the formatting. It doesn't matter what the
359
        // exact values are just that they are returned.
360
        $this->assertObjectHasProperty('icon', $actual);
361
        $this->assertObjectHasProperty('key', $actual->icon);
362
        $this->assertObjectHasProperty('component', $actual->icon);
363
        $this->assertObjectHasProperty('alttext', $actual->icon);
364
    }
365
 
366
    /**
367
     * get_random_question_summaries should allow limiting and offsetting of the result set.
368
     */
11 efrain 369
    public function test_get_random_question_summaries_with_limit_and_offset(): void {
1 efrain 370
        $this->resetAfterTest();
371
        $numberofquestions = 5;
372
        $includesubcategories = false;
373
        $tagids = [];
374
        $limit = 1;
375
        $offset = 0;
376
        $context = \context_system::instance();
377
        list($category, $questions) = $this->create_category_and_questions($numberofquestions);
378
 
379
        // Sort the questions by id to match the ordering of the result.
380
        usort($questions, function($a, $b) {
381
            $aid = $a->id;
382
            $bid = $b->id;
383
 
384
            if ($aid == $bid) {
385
                return 0;
386
            }
387
            return $aid < $bid ? -1 : 1;
388
        });
389
 
390
        for ($i = 0; $i < $numberofquestions; $i++) {
391
            $result = core_question_external::get_random_question_summaries(
392
                $category->id,
393
                $includesubcategories,
394
                $tagids,
395
                $context->id,
396
                $limit,
397
                $offset
398
            );
399
 
400
            $resultquestions = $result['questions'];
401
            $totalcount = $result['totalcount'];
402
 
403
            $this->assertCount($limit, $resultquestions);
404
            $this->assertEquals($numberofquestions, $totalcount);
405
            $actual = array_shift($resultquestions);
406
            $expected = $questions[$i];
407
            $this->assertEquals($expected->id, $actual->id);
408
            $offset++;
409
        }
410
    }
411
 
412
    /**
413
     * get_random_question_summaries should throw an exception if the user doesn't
414
     * have the capability to use the questions in the requested category.
415
     */
11 efrain 416
    public function test_get_random_question_summaries_without_capability(): void {
1 efrain 417
        $this->resetAfterTest();
418
        $generator = $this->getDataGenerator();
419
        $user = $generator->create_user();
420
        $roleid = $generator->create_role();
421
        $systemcontext = \context_system::instance();
422
        $numberofquestions = 5;
423
        $includesubcategories = false;
424
        $tagids = [];
425
        $context = \context_system::instance();
426
        list($category, $questions) = $this->create_category_and_questions($numberofquestions);
427
        $categorycontext = \context::instance_by_id($category->contextid);
428
 
429
        $generator->role_assign($roleid, $user->id, $systemcontext->id);
430
        // Prohibit all of the tag capabilities.
431
        assign_capability('moodle/question:viewall', CAP_PROHIBIT, $roleid, $categorycontext->id);
432
 
433
        $this->setUser($user);
434
        $this->expectException('moodle_exception');
435
        core_question_external::get_random_question_summaries(
436
            $category->id,
437
            $includesubcategories,
438
            $tagids,
439
            $context->id
440
        );
441
    }
442
 
443
    /**
444
     * Create a question category and create questions in that category. Tag
445
     * the first question in each category with the given tags.
446
     *
447
     * @param int $questioncount How many questions to create.
448
     * @param string[] $tagnames The list of tags to use.
449
     * @param stdClass|null $parentcategory The category to set as the parent of the created category.
450
     * @return array The category and questions.
451
     */
452
    protected function create_category_and_questions($questioncount, $tagnames = [], $parentcategory = null) {
453
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
454
 
455
        if ($parentcategory) {
456
            $catparams = ['parent' => $parentcategory->id];
457
        } else {
458
            $catparams = [];
459
        }
460
 
461
        $category = $generator->create_question_category($catparams);
462
        $questions = [];
463
 
464
        for ($i = 0; $i < $questioncount; $i++) {
465
            $questions[] = $generator->create_question('shortanswer', null, ['category' => $category->id]);
466
        }
467
 
468
        if (!empty($tagnames) && !empty($questions)) {
469
            $context = \context::instance_by_id($category->contextid);
470
            \core_tag_tag::set_item_tags('core_question', 'question', $questions[0]->id, $context, $tagnames);
471
        }
472
 
473
        return [$category, $questions];
474
    }
475
}