Proyectos de Subversion Moodle

Rev

| 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_external;
18
 
19
/**
20
 * Unit tests for core_external\external_api.
21
 *
22
 * @package     core_external
23
 * @category    test
24
 * @copyright   2022 Andrew Lyons <andrew@nicols.co.uk>
25
 * @license     http://www.gnu.org/copyleft/gpl.html GNU Public License
26
 * @covers      \core_external\external_api
27
 */
28
class external_api_test extends \advanced_testcase {
29
    /**
30
     * Test the validate_parameters method.
31
     *
32
     * @covers \core_external\external_api::validate_parameters
33
     */
34
    public function test_validate_params(): void {
35
        $params = ['text' => 'aaa', 'someid' => '6'];
36
        $description = new external_function_parameters([
37
            'someid' => new external_value(PARAM_INT, 'Some int value'),
38
            'text'   => new external_value(PARAM_ALPHA, 'Some text value'),
39
        ]);
40
        $result = external_api::validate_parameters($description, $params);
41
        $this->assertCount(2, $result);
42
        reset($result);
43
        $this->assertSame('someid', key($result));
44
        $this->assertSame(6, $result['someid']);
45
        $this->assertSame('aaa', $result['text']);
46
 
47
        $params = [
48
            'someids' => ['1', 2, 'a' => '3'],
49
            'scalar' => 666,
50
        ];
51
        $description = new external_function_parameters([
52
            'someids' => new external_multiple_structure(new external_value(PARAM_INT, 'Some ID')),
53
            'scalar'  => new external_value(PARAM_ALPHANUM, 'Some text value'),
54
        ]);
55
        $result = external_api::validate_parameters($description, $params);
56
        $this->assertCount(2, $result);
57
        reset($result);
58
        $this->assertSame('someids', key($result));
59
        $this->assertEquals([0 => 1, 1 => 2, 2 => 3], $result['someids']);
60
        $this->assertSame('666', $result['scalar']);
61
 
62
        $params = ['text' => 'aaa'];
63
        $description = new external_function_parameters([
64
            'someid' => new external_value(PARAM_INT, 'Some int value', VALUE_DEFAULT),
65
            'text'   => new external_value(PARAM_ALPHA, 'Some text value'),
66
        ]);
67
        $result = external_api::validate_parameters($description, $params);
68
        $this->assertCount(2, $result);
69
        reset($result);
70
        $this->assertSame('someid', key($result));
71
        $this->assertNull($result['someid']);
72
        $this->assertSame('aaa', $result['text']);
73
 
74
        $params = ['text' => 'aaa'];
75
        $description = new external_function_parameters([
76
            'someid' => new external_value(PARAM_INT, 'Some int value', VALUE_DEFAULT, 6),
77
            'text'   => new external_value(PARAM_ALPHA, 'Some text value'),
78
        ]);
79
        $result = external_api::validate_parameters($description, $params);
80
        $this->assertCount(2, $result);
81
        reset($result);
82
        $this->assertSame('someid', key($result));
83
        $this->assertSame(6, $result['someid']);
84
        $this->assertSame('aaa', $result['text']);
85
 
86
        // Missing required value (an exception is thrown).
87
        $testdata = [];
88
        try {
89
            external_api::clean_returnvalue($description, $testdata);
90
            $this->fail('Exception expected');
91
        } catch (\moodle_exception $ex) {
92
            $this->assertInstanceOf(\invalid_response_exception::class, $ex);
93
            $this->assertSame('Invalid response value detected (Error in response - '
94
                . 'Missing following required key in a single structure: text)', $ex->getMessage());
95
        }
96
 
97
        // Test nullable external_value may optionally return data.
98
        $description = new external_function_parameters([
99
            'value' => new external_value(PARAM_INT, '', VALUE_REQUIRED, null, NULL_ALLOWED)
100
        ]);
101
        $testdata = ['value' => null];
102
        $cleanedvalue = external_api::clean_returnvalue($description, $testdata);
103
        $this->assertSame($testdata, $cleanedvalue);
104
        $testdata = ['value' => 1];
105
        $cleanedvalue = external_api::clean_returnvalue($description, $testdata);
106
        $this->assertSame($testdata, $cleanedvalue);
107
 
108
        // Test nullable external_single_structure may optionally return data.
109
        $description = new external_function_parameters([
110
            'value' => new external_single_structure(['value2' => new external_value(PARAM_INT)],
111
                '', VALUE_REQUIRED, null, NULL_ALLOWED)
112
        ]);
113
        $testdata = ['value' => null];
114
        $cleanedvalue = external_api::clean_returnvalue($description, $testdata);
115
        $this->assertSame($testdata, $cleanedvalue);
116
        $testdata = ['value' => ['value2' => 1]];
117
        $cleanedvalue = external_api::clean_returnvalue($description, $testdata);
118
        $this->assertSame($testdata, $cleanedvalue);
119
 
120
        // Test nullable external_multiple_structure may optionally return data.
121
        $description = new external_function_parameters([
122
            'value' => new external_multiple_structure(
123
                new external_value(PARAM_INT), '', VALUE_REQUIRED, null, NULL_ALLOWED)
124
        ]);
125
        $testdata = ['value' => null];
126
        $cleanedvalue = external_api::clean_returnvalue($description, $testdata);
127
        $this->assertSame($testdata, $cleanedvalue);
128
        $testdata = ['value' => [1]];
129
        $cleanedvalue = external_api::clean_returnvalue($description, $testdata);
130
        $this->assertSame($testdata, $cleanedvalue);
131
    }
132
 
133
    /**
134
     * Test for clean_returnvalue() for testing that returns the PHP type.
135
     *
136
     * @covers \core_external\external_api::clean_returnvalue
137
     */
138
    public function test_clean_returnvalue_return_php_type(): void {
139
        $returndesc = new external_single_structure([
140
            'value' => new external_value(PARAM_RAW, 'Some text', VALUE_OPTIONAL, null, NULL_NOT_ALLOWED),
141
        ]);
142
 
143
        // Check return type on exception because the external values does not allow NULL values.
144
        $testdata = ['value' => null];
145
        try {
146
            $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
147
        } catch (\moodle_exception $e) {
148
            $this->assertInstanceOf(\invalid_response_exception::class, $e);
149
            $this->assertStringContainsString('of PHP type "NULL"', $e->debuginfo);
150
        }
151
    }
152
 
153
    /**
154
     * Test for clean_returnvalue().
155
     *
156
     * @covers \core_external\external_api::clean_returnvalue
157
     */
158
    public function test_clean_returnvalue(): void {
159
        // Build some return value decription.
160
        $returndesc = new external_multiple_structure(
161
            new external_single_structure(
162
                [
163
                    'object' => new external_single_structure(
164
                                ['value1' => new external_value(PARAM_INT, 'this is a int')]),
165
                    'value2' => new external_value(PARAM_TEXT, 'some text', VALUE_OPTIONAL),
166
                ]
167
            ));
168
 
169
        // Clean an object (it should be cast into an array).
170
        $object = new \stdClass();
171
        $object->value1 = 1;
172
        $singlestructure['object'] = $object;
173
        $singlestructure['value2'] = 'Some text';
174
        $testdata = [$singlestructure];
175
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
176
        $cleanedsinglestructure = array_pop($cleanedvalue);
177
        $this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']);
178
        $this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']);
179
 
180
        // Missing VALUE_OPTIONAL.
181
        $object = new \stdClass();
182
        $object->value1 = 1;
183
        $singlestructure = new \stdClass();
184
        $singlestructure->object = $object;
185
        $testdata = [$singlestructure];
186
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
187
        $cleanedsinglestructure = array_pop($cleanedvalue);
188
        $this->assertSame($object->value1, $cleanedsinglestructure['object']['value1']);
189
        $this->assertArrayNotHasKey('value2', $cleanedsinglestructure);
190
 
191
        // Unknown attribute (the value should be ignored).
192
        $object = [];
193
        $object['value1'] = 1;
194
        $singlestructure = [];
195
        $singlestructure['object'] = $object;
196
        $singlestructure['value2'] = 'Some text';
197
        $singlestructure['unknownvalue'] = 'Some text to ignore';
198
        $testdata = [$singlestructure];
199
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
200
        $cleanedsinglestructure = array_pop($cleanedvalue);
201
        $this->assertSame($object['value1'], $cleanedsinglestructure['object']['value1']);
202
        $this->assertSame($singlestructure['value2'], $cleanedsinglestructure['value2']);
203
        $this->assertArrayNotHasKey('unknownvalue', $cleanedsinglestructure);
204
 
205
        // Missing required value (an exception is thrown).
206
        $object = [];
207
        $singlestructure = [];
208
        $singlestructure['object'] = $object;
209
        $singlestructure['value2'] = 'Some text';
210
        $testdata = [$singlestructure];
211
        try {
212
            external_api::clean_returnvalue($returndesc, $testdata);
213
            $this->fail('Exception expected');
214
        } catch (\moodle_exception $ex) {
215
            $this->assertInstanceOf(\invalid_response_exception::class, $ex);
216
            $this->assertSame('Invalid response value detected (object => Invalid response value detected '
217
                . '(Error in response - Missing following required key in a single structure: value1): Error in response - '
218
                . 'Missing following required key in a single structure: value1)', $ex->getMessage());
219
        }
220
 
221
        // Fail if no data provided when value required.
222
        $testdata = null;
223
        try {
224
            external_api::clean_returnvalue($returndesc, $testdata);
225
            $this->fail('Exception expected');
226
        } catch (\moodle_exception $ex) {
227
            $this->assertInstanceOf(\invalid_response_exception::class, $ex);
228
            $this->assertSame('Invalid response value detected (Only arrays accepted. The bad value is: \'\')',
229
                $ex->getMessage());
230
        }
231
 
232
        // Test nullable external_multiple_structure may optionally return data.
233
        $returndesc = new external_multiple_structure(
234
            new external_value(PARAM_INT),
235
            '', VALUE_REQUIRED, null, NULL_ALLOWED);
236
        $testdata = null;
237
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
238
        $this->assertSame($testdata, $cleanedvalue);
239
        $testdata = [1];
240
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
241
        $this->assertSame($testdata, $cleanedvalue);
242
 
243
        // Test nullable external_single_structure may optionally return data.
244
        $returndesc = new external_single_structure(['value' => new external_value(PARAM_INT)],
245
            '', VALUE_REQUIRED, null, NULL_ALLOWED);
246
        $testdata = null;
247
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
248
        $this->assertSame($testdata, $cleanedvalue);
249
        $testdata = ['value' => 1];
250
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
251
        $this->assertSame($testdata, $cleanedvalue);
252
 
253
        // Test nullable external_value may optionally return data.
254
        $returndesc = new external_value(PARAM_INT, '', VALUE_REQUIRED, null, NULL_ALLOWED);
255
        $testdata = null;
256
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
257
        $this->assertSame($testdata, $cleanedvalue);
258
        $testdata = 1;
259
        $cleanedvalue = external_api::clean_returnvalue($returndesc, $testdata);
260
        $this->assertSame($testdata, $cleanedvalue);
261
    }
262
 
263
    /**
264
     * Test \core_external\external_api::get_context_from_params().
265
     *
266
     * @covers \core_external\external_api::get_context_from_params
267
     */
268
    public function test_get_context_from_params(): void {
269
        $this->resetAfterTest(true);
270
        $course = $this->getDataGenerator()->create_course();
271
        $realcontext = \context_course::instance($course->id);
272
 
273
        // Use context id.
274
        $fetchedcontext = $this->get_context_from_params(["contextid" => $realcontext->id]);
275
        $this->assertEquals($realcontext, $fetchedcontext);
276
 
277
        // Use context level and instance id.
278
        $fetchedcontext = $this->get_context_from_params(["contextlevel" => "course", "instanceid" => $course->id]);
279
        $this->assertEquals($realcontext, $fetchedcontext);
280
 
281
        // Use context level numbers instead of legacy short level names.
282
        $fetchedcontext = $this->get_context_from_params(
283
            ["contextlevel" => \core\context\course::LEVEL, "instanceid" => $course->id]);
284
        $this->assertEquals($realcontext, $fetchedcontext);
285
 
286
        // Passing empty values.
287
        try {
288
            $fetchedcontext = $this->get_context_from_params(["contextid" => 0]);
289
            $this->fail('Exception expected from get_context_wrapper()');
290
        } catch (\moodle_exception $e) {
291
            $this->assertInstanceOf(\invalid_parameter_exception::class, $e);
292
        }
293
 
294
        try {
295
            $fetchedcontext = $this->get_context_from_params(["instanceid" => 0]);
296
            $this->fail('Exception expected from get_context_wrapper()');
297
        } catch (\moodle_exception $e) {
298
            $this->assertInstanceOf(\invalid_parameter_exception::class, $e);
299
        }
300
 
301
        try {
302
            $fetchedcontext = $this->get_context_from_params(["contextid" => null]);
303
            $this->fail('Exception expected from get_context_wrapper()');
304
        } catch (\moodle_exception $e) {
305
            $this->assertInstanceOf(\invalid_parameter_exception::class, $e);
306
        }
307
 
308
        // Tests for context with instanceid equal to 0 (System context).
309
        $realcontext = \context_system::instance();
310
        $fetchedcontext = $this->get_context_from_params(["contextlevel" => "system", "instanceid" => 0]);
311
        $this->assertEquals($realcontext, $fetchedcontext);
312
 
313
        // Passing wrong level name.
314
        try {
315
            $fetchedcontext = $this->get_context_from_params(["contextlevel" => "random", "instanceid" => $course->id]);
316
            $this->fail('exception expected when level name is invalid');
317
        } catch (\moodle_exception $e) {
318
            $this->assertInstanceOf('invalid_parameter_exception', $e);
319
            $this->assertSame('Invalid parameter value detected (Invalid context level = random)', $e->getMessage());
320
        }
321
 
322
        // Passing wrong level number.
323
        try {
324
            $fetchedcontext = $this->get_context_from_params(["contextlevel" => -10, "instanceid" => $course->id]);
325
            $this->fail('exception expected when level name is invalid');
326
        } catch (\moodle_exception $e) {
327
            $this->assertInstanceOf('invalid_parameter_exception', $e);
328
            $this->assertSame('Invalid parameter value detected (Invalid context level = -10)', $e->getMessage());
329
        }
330
    }
331
 
332
    /**
333
     * Test \core_external\external_api::get_context()_from_params parameter validation.
334
     *
335
     * @covers \core_external\external_api::get_context
336
     */
337
    public function test_get_context_params(): void {
338
        global $USER;
339
 
340
        // Call without correct context details.
341
        $this->expectException('invalid_parameter_exception');
342
        $this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id]);
343
    }
344
 
345
    /**
346
     * Test \core_external\external_api::get_context()_from_params parameter validation.
347
     *
348
     * @covers \core_external\external_api::get_context
349
     */
350
    public function test_get_context_params2(): void {
351
        global $USER;
352
 
353
        // Call without correct context details.
354
        $this->expectException('invalid_parameter_exception');
355
        $this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id, 'contextlevel' => "course"]);
356
    }
357
 
358
    /**
359
     * Test \core_external\external_api::get_context()_from_params parameter validation.
360
     * @covers \core_external\external_api::get_context
361
     */
362
    public function test_get_context_params3(): void {
363
        global $USER;
364
 
365
        // Call without correct context details.
366
        $this->resetAfterTest(true);
367
        $course = self::getDataGenerator()->create_course();
368
        $this->expectException('invalid_parameter_exception');
369
        $this->get_context_from_params(['roleid' => 3, 'userid' => $USER->id, 'instanceid' => $course->id]);
370
    }
371
 
372
    /**
373
     * Data provider for the test_all_external_info test.
374
     *
375
     * @return array
376
     */
377
    public function all_external_info_provider(): array {
378
        global $DB;
379
 
380
        // We are testing here that all the external function descriptions can be generated without
381
        // producing warnings. E.g. misusing optional params will generate a debugging message which
382
        // will fail this test.
383
        $functions = $DB->get_records('external_functions', [], 'name');
384
        $return = [];
385
        foreach ($functions as $f) {
386
            $return[$f->name] = [$f];
387
        }
388
        return $return;
389
    }
390
 
391
    /**
392
     * Test \core_external\external_api::external_function_info.
393
     *
394
     * @runInSeparateProcess
395
     * @dataProvider all_external_info_provider
396
     * @covers \core_external\external_api::external_function_info
397
     * @param \stdClass $definition
398
     */
399
    public function test_all_external_info(\stdClass $definition): void {
400
        $desc = external_api::external_function_info($definition);
401
        $this->assertNotEmpty($desc->name);
402
        $this->assertNotEmpty($desc->classname);
403
        $this->assertNotEmpty($desc->methodname);
404
        $this->assertEquals($desc->component, clean_param($desc->component, PARAM_COMPONENT));
405
        $this->assertInstanceOf(external_function_parameters::class, $desc->parameters_desc);
406
        if ($desc->returns_desc != null) {
407
            $this->assertInstanceOf(external_description::class, $desc->returns_desc);
408
        }
409
    }
410
 
411
    /**
412
     * Test the \core_external\external_api::call_external_function() function.
413
     *
414
     * @covers \core_external\external_api::call_external_function
415
     */
416
    public function test_call_external_function(): void {
417
        global $PAGE, $COURSE, $CFG;
418
 
419
        $this->resetAfterTest(true);
420
 
421
        // Call some webservice functions and verify they are correctly handling $PAGE and $COURSE.
422
        // First test a function that calls validate_context outside a course.
423
        $this->setAdminUser();
424
        $category = $this->getDataGenerator()->create_category();
425
        $params = [
426
            'contextid' => \context_coursecat::instance($category->id)->id,
427
            'name' => 'aaagrrryyy',
428
            'idnumber' => '',
429
            'description' => '',
430
        ];
431
        $cohort1 = $this->getDataGenerator()->create_cohort($params);
432
        $cohort2 = $this->getDataGenerator()->create_cohort();
433
 
434
        $beforepage = $PAGE;
435
        $beforecourse = $COURSE;
436
        $params = ['cohortids' => [$cohort1->id, $cohort2->id]];
437
        $result = external_api::call_external_function('core_cohort_get_cohorts', $params);
438
 
439
        $this->assertSame($beforepage, $PAGE);
440
        $this->assertSame($beforecourse, $COURSE);
441
 
442
        // Now test a function that calls validate_context inside a course.
443
        $course = $this->getDataGenerator()->create_course();
444
 
445
        $beforepage = $PAGE;
446
        $beforecourse = $COURSE;
447
        $params = ['courseid' => $course->id, 'options' => []];
448
        $result = external_api::call_external_function('core_enrol_get_enrolled_users', $params);
449
 
450
        $this->assertSame($beforepage, $PAGE);
451
        $this->assertSame($beforecourse, $COURSE);
452
 
453
        // Test a function that triggers a PHP exception.
454
        require_once($CFG->dirroot . '/lib/tests/fixtures/test_external_function_throwable.php');
455
 
456
        // Call our test function.
457
        $result = \test_external_function_throwable::call_external_function('core_throw_exception', [], false);
458
 
459
        $this->assertTrue($result['error']);
460
        $this->assertArrayHasKey('exception', $result);
461
        $this->assertEquals($result['exception']->message, 'Exception - Modulo by zero');
462
    }
463
 
464
    /**
465
     * Call the get_contect_from_params methods on the api class.
466
     *
467
     * @return mixed
468
     */
469
    protected function get_context_from_params() {
470
        $rc = new \ReflectionClass(external_api::class);
471
        $method = $rc->getMethod('get_context_from_params');
472
        return $method->invokeArgs(null, func_get_args());
473
    }
474
}