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