lib_test.php 15.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Unit tests for the webservice component.
 *
 * @package    core_webservice
 * @category   test
 * @copyright  2016 Jun Pataleta <jun@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
defined('MOODLE_INTERNAL') || die();

global $CFG;
require_once($CFG->dirroot . '/webservice/lib.php');

/**
 * Unit tests for the webservice component.
 *
 * @package    core_webservice
 * @category   test
 * @copyright  2016 Jun Pataleta <jun@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class webservice_test extends advanced_testcase {

    /**
     * Setup.
     */
43
    public function setUp(): void {
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
        // Calling parent is good, always.
        parent::setUp();

        // We always need enabled WS for this testcase.
        set_config('enablewebservices', '1');
    }

    /**
     * Test init_service_class().
     */
    public function test_init_service_class() {
        global $DB, $USER;

        $this->resetAfterTest(true);

        // Set current user.
        $this->setAdminUser();

        // Add a web service.
        $webservice = new stdClass();
        $webservice->name = 'Test web service';
        $webservice->enabled = true;
        $webservice->restrictedusers = false;
        $webservice->component = 'moodle';
        $webservice->timecreated = time();
        $webservice->downloadfiles = true;
        $webservice->uploadfiles = true;
        $externalserviceid = $DB->insert_record('external_services', $webservice);

        // Add token.
        $externaltoken = new stdClass();
        $externaltoken->token = 'testtoken';
        $externaltoken->tokentype = 0;
        $externaltoken->userid = $USER->id;
        $externaltoken->externalserviceid = $externalserviceid;
        $externaltoken->contextid = 1;
        $externaltoken->creatorid = $USER->id;
        $externaltoken->timecreated = time();
        $DB->insert_record('external_tokens', $externaltoken);

        // Add a function to the service.
        $wsmethod = new stdClass();
        $wsmethod->externalserviceid = $externalserviceid;
        $wsmethod->functionname = 'core_course_get_contents';
        $DB->insert_record('external_services_functions', $wsmethod);

        // Initialise the dummy web service.
        $dummy = new webservice_dummy(WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN);
        // Set the token.
        $dummy->set_token($externaltoken->token);
        // Run the web service.
        $dummy->run();
        // Get service methods and structs.
        $servicemethods = $dummy->get_service_methods();
        $servicestructs = $dummy->get_service_structs();
        $this->assertNotEmpty($servicemethods);
        // The function core_course_get_contents should be only the only web service function in the moment.
        $this->assertEquals(1, count($servicemethods));
        // The function core_course_get_contents doesn't have a struct class, so the list of service structs should be empty.
        $this->assertEmpty($servicestructs);

        // Add other functions to the service.
        // The function core_comment_get_comments has one struct class in its output.
        $wsmethod->functionname = 'core_comment_get_comments';
        $DB->insert_record('external_services_functions', $wsmethod);
        // The function core_grades_update_grades has one struct class in its input.
        $wsmethod->functionname = 'core_grades_update_grades';
        $DB->insert_record('external_services_functions', $wsmethod);

        // Run the web service again.
        $dummy->run();
        // Get service methods and structs.
        $servicemethods = $dummy->get_service_methods();
        $servicestructs = $dummy->get_service_structs();
        $this->assertEquals(3, count($servicemethods));
        $this->assertEquals(2, count($servicestructs));

        // Check the contents of service methods.
        foreach ($servicemethods as $method) {
            // Get the external function info.
124
            $function = external_api::external_function_info($method->name);
125
126
127
128
129
130
131
132
133
134
135
136
137
138

            // Check input params.
            foreach ($function->parameters_desc->keys as $name => $keydesc) {
                $this->check_params($method->inputparams[$name]['type'], $keydesc, $servicestructs);
            }

            // Check output params.
            $this->check_params($method->outputparams['return']['type'], $function->returns_desc, $servicestructs);

            // Check description.
            $this->assertEquals($function->description, $method->description);
        }
    }

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
    /**
     * Tests update_token_lastaccess() function.
     *
     * @throws dml_exception
     */
    public function test_update_token_lastaccess() {
        global $DB;

        $this->resetAfterTest(true);

        // Set current user.
        $this->setAdminUser();

        // Add a web service.
        $webservice = new stdClass();
        $webservice->name = 'Test web service';
        $webservice->enabled = true;
        $webservice->restrictedusers = false;
        $webservice->component = 'moodle';
        $webservice->timecreated = time();
        $webservice->downloadfiles = true;
        $webservice->uploadfiles = true;
        $DB->insert_record('external_services', $webservice);

        // Add token.
        $tokenstr = external_create_service_token($webservice->name, context_system::instance()->id);
        $token = $DB->get_record('external_tokens', ['token' => $tokenstr]);

        // Trigger last access once (at current time).
        webservice::update_token_lastaccess($token);

        // Check last access.
        $token = $DB->get_record('external_tokens', ['token' => $tokenstr]);
        $this->assertLessThan(5, abs(time() - $token->lastaccess));

        // Try setting it to +1 second. This should not update yet.
        $before = (int)$token->lastaccess;
        webservice::update_token_lastaccess($token, $before + 1);
        $token = $DB->get_record('external_tokens', ['token' => $tokenstr]);
        $this->assertEquals($before, $token->lastaccess);

        // To -1000 seconds. This should not update.
        webservice::update_token_lastaccess($token, $before - 1000);
        $token = $DB->get_record('external_tokens', ['token' => $tokenstr]);
        $this->assertEquals($before, $token->lastaccess);

        // To +59 seconds. This should also not quite update.
        webservice::update_token_lastaccess($token, $before + 59);
        $token = $DB->get_record('external_tokens', ['token' => $tokenstr]);
        $this->assertEquals($before, $token->lastaccess);

        // Finally to +60 seconds, where it should update.
        webservice::update_token_lastaccess($token, $before + 60);
        $token = $DB->get_record('external_tokens', ['token' => $tokenstr]);
        $this->assertEquals($before + 60, $token->lastaccess);
    }

196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
    /**
     * Tests for the {@see webservice::get_missing_capabilities_by_users()} implementation.
     */
    public function test_get_missing_capabilities_by_users() {
        global $DB;

        $this->resetAfterTest(true);
        $wsman = new webservice();

        $user1 = $this->getDataGenerator()->create_user();
        $user2 = $this->getDataGenerator()->create_user();
        $user3 = $this->getDataGenerator()->create_user();

        // Add a test web service.
        $serviceid = $wsman->add_external_service((object)[
            'name' => 'Test web service',
            'enabled' => 1,
            'requiredcapability' => '',
            'restrictedusers' => false,
            'component' => 'moodle',
            'downloadfiles' => false,
            'uploadfiles' => false,
        ]);

        // Add a function to the service that does not declare any capability as required.
        $wsman->add_external_function_to_service('core_webservice_get_site_info', $serviceid);

        // Users can be provided as an array of objects, arrays or integers (ids).
        $this->assertEmpty($wsman->get_missing_capabilities_by_users([$user1, array($user2), $user3->id], $serviceid));

        // Add a function to the service that declares some capability as required, but that capability is common for
        // any user. Here we use 'core_message_delete_conversation' which declares 'moodle/site:deleteownmessage' which
        // in turn is granted to the authenticated user archetype by default.
        $wsman->add_external_function_to_service('core_message_delete_conversation', $serviceid);

        // So all three users should have this capability implicitly.
        $this->assertEmpty($wsman->get_missing_capabilities_by_users([$user1, $user2, $user3], $serviceid));

        // Add a function to the service that declares some non-common capability. Here we use
        // 'core_group_add_group_members' that wants 'moodle/course:managegroups'.
        $wsman->add_external_function_to_service('core_group_add_group_members', $serviceid);

        // Make it so that the $user1 has the capability in some course.
        $course1 = $this->getDataGenerator()->create_course();
        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'editingteacher');

        // Check that no missing capability is reported for the $user1. We don't care at what actual context the
        // external function call will evaluate the permission. We just check that there is a chance that the user has
        // the capability somewhere.
        $this->assertEmpty($wsman->get_missing_capabilities_by_users([$user1], $serviceid));

        // But there is no place at the site where the capability would be granted to the other two users, so it is
        // reported as missing.
        $missing = $wsman->get_missing_capabilities_by_users([$user1, $user2, $user3], $serviceid);
        $this->assertArrayNotHasKey($user1->id, $missing);
        $this->assertContains('moodle/course:managegroups', $missing[$user2->id]);
        $this->assertContains('moodle/course:managegroups', $missing[$user3->id]);
    }

255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
    /**
     * Utility method that tests the parameter type of a method info's input/output parameter.
     *
     * @param string $type The parameter type that is being evaluated.
     * @param mixed $methoddesc The method description of the WS function.
     * @param array $servicestructs The list of generated service struct classes.
     */
    private function check_params($type, $methoddesc, $servicestructs) {
        if ($methoddesc instanceof external_value) {
            // Test for simple types.
            if (in_array($methoddesc->type, [PARAM_INT, PARAM_FLOAT, PARAM_BOOL])) {
                $this->assertEquals($methoddesc->type, $type);
            } else {
                $this->assertEquals('string', $type);
            }
        } else if ($methoddesc instanceof external_single_structure) {
            // Test that the class name of the struct class is in the array of service structs.
            $structinfo = $this->get_struct_info($servicestructs, $type);
            $this->assertNotNull($structinfo);
            // Test that the properties of the struct info exist in the method description.
            foreach ($structinfo->properties as $propname => $proptype) {
                $this->assertTrue($this->in_keydesc($methoddesc, $propname));
            }
        } else if ($methoddesc instanceof external_multiple_structure) {
            // Test for array types.
            $this->assertEquals('array', $type);
        }
    }

    /**
     * Gets the struct information from the list of struct classes based on the given struct class name.
     *
     * @param array $structarray The list of generated struct classes.
     * @param string $structclass The name of the struct class.
     * @return object|null The struct class info, or null if it's not found.
     */
    private function get_struct_info($structarray, $structclass) {
        foreach ($structarray as $struct) {
            if ($struct->classname === $structclass) {
                return $struct;
            }
        }
        return null;
    }

    /**
     * Searches the keys of the given external_single_structure object if it contains a certain property name.
     *
     * @param external_single_structure $keydesc
     * @param string $propertyname The property name to be searched for.
     * @return bool True if the property name is found in $keydesc. False, otherwise.
     */
    private function in_keydesc(external_single_structure $keydesc, $propertyname) {
        foreach ($keydesc->keys as $key => $desc) {
            if ($key === $propertyname) {
                return true;
            }
        }
        return false;
    }
}

/**
 * Class webservice_dummy.
 *
 * Dummy webservice class for testing the webservice_base_server class and enable us to expose variables we want to test.
 *
 * @package    core_webservice
 * @category   test
 * @copyright  2016 Jun Pataleta <jun@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class webservice_dummy extends webservice_base_server {

    /**
     * webservice_dummy constructor.
     *
     * @param int $authmethod The authentication method.
     */
    public function __construct($authmethod) {
        parent::__construct($authmethod);

        // Arbitrarily naming this as REST in order not to have to register another WS protocol and set capabilities.
        $this->wsname = 'rest';
    }

    /**
     * Token setter method.
     *
     * @param string $token The web service token.
     */
    public function set_token($token) {
        $this->token = $token;
    }

    /**
     * This method parses the request input, it needs to get:
     *  1/ user authentication - username+password or token
     *  2/ function name
     *  3/ function parameters
     */
    protected function parse_request() {
        // Just a method stub. No need to implement at the moment since it's not really being used for this test case for now.
    }

    /**
     * Send the result of function call to the WS client.
     */
    protected function send_response() {
        // Just a method stub. No need to implement at the moment since it's not really being used for this test case for now.
    }

    /**
     * Send the error information to the WS client.
     *
     * @param exception $ex
     */
    protected function send_error($ex = null) {
        // Just a method stub. No need to implement at the moment since it's not really being used for this test case for now.
    }

    /**
     * run() method implementation.
     */
    public function run() {
        $this->authenticate_user();
        $this->init_service_class();
    }

    /**
     * Getter method of servicemethods array.
     *
     * @return array
     */
    public function get_service_methods() {
        return $this->servicemethods;
    }

    /**
     * Getter method of servicestructs array.
     *
     * @return array
     */
    public function get_service_structs() {
        return $this->servicestructs;
    }
}