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