4045ab14d269f9e575bc4e5d90abe1103f5491e8
[moodle.git] / admin / tool / mobile / tests / externallib_test.php
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/>.
17 /**
18  * Moodle Mobile admin tool external functions tests.
19  *
20  * @package    tool_mobile
21  * @category   external
22  * @copyright  2016 Juan Leyva
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      Moodle 3.1
25  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
31 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
32 require_once($CFG->dirroot . '/admin/tool/mobile/tests/fixtures/output/mobile.php');
33 require_once($CFG->dirroot . '/webservice/lib.php');
35 use tool_mobile\external;
36 use tool_mobile\api;
38 /**
39  * Moodle Mobile admin tool external functions tests.
40  *
41  * @package     tool_mobile
42  * @copyright   2016 Juan Leyva
43  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44  * @since       Moodle 3.1
45  */
46 class tool_mobile_external_testcase extends externallib_advanced_testcase {
48     /**
49      * Test get_plugins_supporting_mobile.
50      * This is a very basic test because currently there aren't plugins supporting Mobile in core.
51      */
52     public function test_get_plugins_supporting_mobile() {
53         $result = external::get_plugins_supporting_mobile();
54         $result = external_api::clean_returnvalue(external::get_plugins_supporting_mobile_returns(), $result);
55         $this->assertCount(0, $result['warnings']);
56         $this->assertArrayHasKey('plugins', $result);
57         $this->assertTrue(is_array($result['plugins']));
58     }
60     public function test_get_public_config() {
61         global $CFG, $SITE, $OUTPUT;
63         $this->resetAfterTest(true);
64         $result = external::get_public_config();
65         $result = external_api::clean_returnvalue(external::get_public_config_returns(), $result);
67         // Test default values.
68         $context = context_system::instance();
69         list($authinstructions, $notusedformat) = external_format_text($CFG->auth_instructions, FORMAT_MOODLE, $context->id);
70         list($maintenancemessage, $notusedformat) = external_format_text($CFG->maintenance_message, FORMAT_MOODLE, $context->id);
72         $expected = array(
73             'wwwroot' => $CFG->wwwroot,
74             'httpswwwroot' => $CFG->httpswwwroot,
75             'sitename' => external_format_string($SITE->fullname, $context->id, true),
76             'guestlogin' => $CFG->guestloginbutton,
77             'rememberusername' => $CFG->rememberusername,
78             'authloginviaemail' => $CFG->authloginviaemail,
79             'registerauth' => $CFG->registerauth,
80             'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
81             'authinstructions' => $authinstructions,
82             'authnoneenabled' => (int) is_enabled_auth('none'),
83             'enablewebservices' => $CFG->enablewebservices,
84             'enablemobilewebservice' => $CFG->enablemobilewebservice,
85             'maintenanceenabled' => $CFG->maintenance_enabled,
86             'maintenancemessage' => $maintenancemessage,
87             'typeoflogin' => api::LOGIN_VIA_APP,
88             'mobilecssurl' => '',
89             'tool_mobile_disabledfeatures' => '',
90             'launchurl' => "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php",
91             'country' => $CFG->country,
92             'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
93             'autolang' => $CFG->autolang,
94             'lang' => $CFG->lang,
95             'langmenu' => $CFG->langmenu,
96             'langlist' => $CFG->langlist,
97             'locale' => $CFG->locale,
98             'warnings' => array()
99         );
100         $this->assertEquals($expected, $result);
102         // Change some values.
103         set_config('registerauth', 'email');
104         $authinstructions = 'Something with <b>html tags</b>';
105         set_config('auth_instructions', $authinstructions);
106         set_config('typeoflogin', api::LOGIN_VIA_BROWSER, 'tool_mobile');
107         set_config('logo', 'mock.png', 'core_admin');
108         set_config('logocompact', 'mock.png', 'core_admin');
109         set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
110         set_config('agedigitalconsentverification', 1);
111         set_config('autolang', 1);
112         set_config('lang', 'a_b');  // Set invalid lang.
113         set_config('disabledfeatures', 'myoverview', 'tool_mobile');
115         list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
116         $expected['registerauth'] = 'email';
117         $expected['authinstructions'] = $authinstructions;
118         $expected['typeoflogin'] = api::LOGIN_VIA_BROWSER;
119         $expected['forgottenpasswordurl'] = ''; // Expect empty when it's not an URL.
120         $expected['agedigitalconsentverification'] = true;
121         $expected['supportname'] = $CFG->supportname;
122         $expected['supportemail'] = $CFG->supportemail;
123         $expected['autolang'] = '1';
124         $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
125         $expected['tool_mobile_disabledfeatures'] = 'myoverview';
127         if ($logourl = $OUTPUT->get_logo_url()) {
128             $expected['logourl'] = $logourl->out(false);
129         }
130         if ($compactlogourl = $OUTPUT->get_compact_logo_url()) {
131             $expected['compactlogourl'] = $compactlogourl->out(false);
132         }
134         $result = external::get_public_config();
135         $result = external_api::clean_returnvalue(external::get_public_config_returns(), $result);
136         $this->assertEquals($expected, $result);
137     }
139     /**
140      * Test get_config
141      */
142     public function test_get_config() {
143         global $CFG, $SITE;
144         require_once($CFG->dirroot . '/course/format/lib.php');
146         $this->resetAfterTest(true);
148         $mysitepolicy = 'http://mysite.is/policy/';
149         set_config('sitepolicy', $mysitepolicy);
151         $result = external::get_config();
152         $result = external_api::clean_returnvalue(external::get_config_returns(), $result);
154         // SITE summary is null in phpunit which gets transformed to an empty string by format_text.
155         list($sitesummary, $unused) = external_format_text($SITE->summary, $SITE->summaryformat, context_system::instance()->id);
157         // Test default values.
158         $context = context_system::instance();
159         $expected = array(
160             array('name' => 'fullname', 'value' => $SITE->fullname),
161             array('name' => 'shortname', 'value' => $SITE->shortname),
162             array('name' => 'summary', 'value' => $sitesummary),
163             array('name' => 'summaryformat', 'value' => FORMAT_HTML),
164             array('name' => 'frontpage', 'value' => $CFG->frontpage),
165             array('name' => 'frontpageloggedin', 'value' => $CFG->frontpageloggedin),
166             array('name' => 'maxcategorydepth', 'value' => $CFG->maxcategorydepth),
167             array('name' => 'frontpagecourselimit', 'value' => $CFG->frontpagecourselimit),
168             array('name' => 'numsections', 'value' => course_get_format($SITE)->get_last_section_number()),
169             array('name' => 'newsitems', 'value' => $SITE->newsitems),
170             array('name' => 'commentsperpage', 'value' => $CFG->commentsperpage),
171             array('name' => 'sitepolicy', 'value' => $mysitepolicy),
172             array('name' => 'sitepolicyhandler', 'value' => ''),
173             array('name' => 'disableuserimages', 'value' => $CFG->disableuserimages),
174             array('name' => 'mygradesurl', 'value' => user_mygrades_url()->out(false)),
175             array('name' => 'tool_mobile_forcelogout', 'value' => 0),
176             array('name' => 'tool_mobile_customlangstrings', 'value' => ''),
177             array('name' => 'tool_mobile_disabledfeatures', 'value' => ''),
178             array('name' => 'tool_mobile_custommenuitems', 'value' => ''),
179             array('name' => 'tool_mobile_apppolicy', 'value' => ''),
180         );
181         $this->assertCount(0, $result['warnings']);
182         $this->assertEquals($expected, $result['settings']);
184         // Change a value and retrieve filtering by section.
185         set_config('commentsperpage', 1);
186         $expected[10]['value'] = 1;
187         // Remove not expected elements.
188         array_splice($expected, 11);
190         $result = external::get_config('frontpagesettings');
191         $result = external_api::clean_returnvalue(external::get_config_returns(), $result);
192         $this->assertCount(0, $result['warnings']);
193         $this->assertEquals($expected, $result['settings']);
194     }
196     /*
197      * Test get_autologin_key.
198      */
199     public function test_get_autologin_key() {
200         global $DB, $CFG, $USER;
202         $this->resetAfterTest(true);
204         $user = $this->getDataGenerator()->create_user();
205         $this->setUser($user);
206         $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
208         $token = external_generate_token_for_current_user($service);
210         // Check we got the private token.
211         $this->assertTrue(isset($token->privatetoken));
213         // Enable requeriments.
214         $_GET['wstoken'] = $token->token;   // Mock parameters.
216         // Even if we force the password change for the current user we should be able to retrieve the key.
217         set_user_preference('auth_forcepasswordchange', 1, $user->id);
219         $this->setCurrentTimeStart();
220         $result = external::get_autologin_key($token->privatetoken);
221         $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
222         // Validate the key.
223         $this->assertEquals(32, core_text::strlen($result['key']));
224         $key = $DB->get_record('user_private_key', array('value' => $result['key']));
225         $this->assertEquals($USER->id, $key->userid);
226         $this->assertTimeCurrent($key->validuntil - api::LOGIN_KEY_TTL);
228         // Now, try with an invalid private token.
229         set_user_preference('tool_mobile_autologin_request_last', time() - HOURSECS, $USER);
231         $this->expectException('moodle_exception');
232         $this->expectExceptionMessage(get_string('invalidprivatetoken', 'tool_mobile'));
233         $result = external::get_autologin_key(random_string('64'));
234     }
236     /**
237      * Test get_autologin_key missing ws.
238      */
239     public function test_get_autologin_key_missing_ws() {
240         global $CFG;
241         $this->resetAfterTest(true);
243         // Need to disable webservices to verify that's checked.
244         $CFG->enablewebservices = 0;
245         $CFG->enablemobilewebservice = 0;
247         $this->setAdminUser();
248         $this->expectException('moodle_exception');
249         $this->expectExceptionMessage(get_string('enablewsdescription', 'webservice'));
250         $result = external::get_autologin_key('');
251     }
253     /**
254      * Test get_autologin_key missing https.
255      */
256     public function test_get_autologin_key_missing_https() {
257         global $CFG;
259         // Need to simulate a non HTTPS site here.
260         $CFG->wwwroot = str_replace('https:', 'http:', $CFG->wwwroot);
262         $this->resetAfterTest(true);
263         $this->setAdminUser();
265         $this->expectException('moodle_exception');
266         $this->expectExceptionMessage(get_string('httpsrequired', 'tool_mobile'));
267         $result = external::get_autologin_key('');
268     }
270     /**
271      * Test get_autologin_key missing admin.
272      */
273     public function test_get_autologin_key_missing_admin() {
274         global $CFG;
276         $this->resetAfterTest(true);
277         $this->setAdminUser();
279         $this->expectException('moodle_exception');
280         $this->expectExceptionMessage(get_string('autologinnotallowedtoadmins', 'tool_mobile'));
281         $result = external::get_autologin_key('');
282     }
284     /**
285      * Test get_autologin_key locked.
286      */
287     public function test_get_autologin_key_missing_locked() {
288         global $CFG, $DB, $USER;
290         $this->resetAfterTest(true);
291         $user = $this->getDataGenerator()->create_user();
292         $this->setUser($user);
294         $service = $DB->get_record('external_services', array('shortname' => MOODLE_OFFICIAL_MOBILE_SERVICE));
296         $token = external_generate_token_for_current_user($service);
297         $_GET['wstoken'] = $token->token;   // Mock parameters.
299         $result = external::get_autologin_key($token->privatetoken);
300         $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
302         // Mock last time request.
303         $mocktime = time() - 7 * MINSECS;
304         set_user_preference('tool_mobile_autologin_request_last', $mocktime, $USER);
305         $result = external::get_autologin_key($token->privatetoken);
306         $result = external_api::clean_returnvalue(external::get_autologin_key_returns(), $result);
308         // We just requested one token, we must wait.
309         $this->expectException('moodle_exception');
310         $this->expectExceptionMessage(get_string('autologinkeygenerationlockout', 'tool_mobile'));
311         $result = external::get_autologin_key($token->privatetoken);
312     }
314     /**
315      * Test get_content.
316      */
317     public function test_get_content() {
319         $paramval = 16;
320         $result = external::get_content('tool_mobile', 'test_view', array(array('name' => 'param1', 'value' => $paramval)));
321         $result = external_api::clean_returnvalue(external::get_content_returns(), $result);
322         $this->assertCount(1, $result['templates']);
323         $this->assertCount(1, $result['otherdata']);
324         $this->assertCount(2, $result['restrict']['users']);
325         $this->assertCount(2, $result['restrict']['courses']);
326         $this->assertEquals('alert();', $result['javascript']);
327         $this->assertEquals('main', $result['templates'][0]['id']);
328         $this->assertEquals('The HTML code', $result['templates'][0]['html']);
329         $this->assertEquals('otherdata1', $result['otherdata'][0]['name']);
330         $this->assertEquals($paramval, $result['otherdata'][0]['value']);
331         $this->assertEquals(array(1, 2), $result['restrict']['users']);
332         $this->assertEquals(array(3, 4), $result['restrict']['courses']);
333         $this->assertEmpty($result['files']);
334     }
336     /**
337      * Test get_content non existent function in valid component.
338      */
339     public function test_get_content_non_existent_function() {
341         $this->expectException('coding_exception');
342         $result = external::get_content('tool_mobile', 'test_blahblah');
343     }
345     /**
346      * Test get_content incorrect component.
347      */
348     public function test_get_content_invalid_component() {
350         $this->expectException('moodle_exception');
351         $result = external::get_content('tool_mobile\hack', 'test_view');
352     }
354     /**
355      * Test get_content non existent component.
356      */
357     public function test_get_content_non_existent_component() {
359         $this->expectException('moodle_exception');
360         $result = external::get_content('tool_blahblahblah', 'test_view');
361     }
363     public function test_call_external_functions() {
364         global $SESSION;
366         $this->resetAfterTest(true);
368         $category = self::getDataGenerator()->create_category(array('name' => 'Category 1'));
369         $course = self::getDataGenerator()->create_course([
370             'category' => $category->id,
371             'shortname' => 'c1',
372             'summary' => '<span lang="en" class="multilang">Course summary</span>'
373                 . '<span lang="eo" class="multilang">Kurso resumo</span>'
374                 . '@@PLUGINFILE@@/filename.txt'
375                 . '<!-- Comment stripped when formatting text -->',
376             'summaryformat' => FORMAT_MOODLE
377         ]);
378         $user1 = self::getDataGenerator()->create_user(['username' => 'user1', 'lastaccess' => time()]);
379         $user2 = self::getDataGenerator()->create_user(['username' => 'user2', 'lastaccess' => time()]);
381         self::setUser($user1);
383         // Setup WS token.
384         $webservicemanager = new \webservice;
385         $service = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
386         $token = external_generate_token_for_current_user($service);
387         $_POST['wstoken'] = $token->token;
389         // Workaround for external_api::call_external_function requiring sesskey.
390         $_POST['sesskey'] = sesskey();
392         // Call some functions.
394         $requests = [
395             [
396                 'function' => 'core_course_get_courses_by_field',
397                 'arguments' => json_encode(['field' => 'id', 'value' => $course->id])
398             ],
399             [
400                 'function' => 'core_user_get_users_by_field',
401                 'arguments' => json_encode(['field' => 'id', 'values' => [$user1->id]])
402             ],
403             [
404                 'function' => 'core_user_get_user_preferences',
405                 'arguments' => json_encode(['name' => 'some_setting', 'userid' => $user2->id])
406             ],
407             [
408                 'function' => 'core_course_get_courses_by_field',
409                 'arguments' => json_encode(['field' => 'shortname', 'value' => $course->shortname])
410             ],
411         ];
412         $result = external::call_external_functions($requests);
414         // We need to execute the return values cleaning process to simulate the web service server.
415         $result = external_api::clean_returnvalue(external::call_external_functions_returns(), $result);
417         // Only 3 responses, the 4th request is not executed because the 3rd throws an exception.
418         $this->assertCount(3, $result['responses']);
420         $this->assertFalse($result['responses'][0]['error']);
421         $coursedata = external_api::clean_returnvalue(
422             core_course_external::get_courses_by_field_returns(),
423             core_course_external::get_courses_by_field('id', $course->id));
424          $this->assertEquals(json_encode($coursedata), $result['responses'][0]['data']);
426         $this->assertFalse($result['responses'][1]['error']);
427         $userdata = external_api::clean_returnvalue(
428             core_user_external::get_users_by_field_returns(),
429             core_user_external::get_users_by_field('id', [$user1->id]));
430         $this->assertEquals(json_encode($userdata), $result['responses'][1]['data']);
432         $this->assertTrue($result['responses'][2]['error']);
433         $exception = json_decode($result['responses'][2]['exception'], true);
434         $this->assertEquals('nopermissions', $exception['errorcode']);
436         // Call a function not included in the external service.
438         $_POST['wstoken'] = $token->token;
439         $functions = $webservicemanager->get_not_associated_external_functions($service->id);
440         $requests = [['function' => current($functions)->name]];
441         $result = external::call_external_functions($requests);
443         $this->assertTrue($result['responses'][0]['error']);
444         $exception = json_decode($result['responses'][0]['exception'], true);
445         $this->assertEquals('accessexception', $exception['errorcode']);
446         $this->assertEquals('webservice', $exception['module']);
448         // Call a function with different external settings.
450         filter_set_global_state('multilang', TEXTFILTER_ON);
451         $_POST['wstoken'] = $token->token;
452         $SESSION->lang = 'eo'; // Change default language, so we can test changing it to "en".
453         $requests = [
454             [
455                 'function' => 'core_course_get_courses_by_field',
456                 'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
457             ],
458             [
459                 'function' => 'core_course_get_courses_by_field',
460                 'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
461                 'settingraw' => '1'
462             ],
463             [
464                 'function' => 'core_course_get_courses_by_field',
465                 'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
466                 'settingraw' => '1',
467                 'settingfileurl' => '0'
468             ],
469             [
470                 'function' => 'core_course_get_courses_by_field',
471                 'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
472                 'settingfilter' => '1',
473                 'settinglang' => 'en'
474             ],
475         ];
476         $result = external::call_external_functions($requests);
478         $this->assertCount(4, $result['responses']);
480         $context = \context_course::instance($course->id);
481         $pluginfile = 'webservice/pluginfile.php';
483         $this->assertFalse($result['responses'][0]['error']);
484         $data = json_decode($result['responses'][0]['data']);
485         $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
486         $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => false]);
487         $this->assertEquals($expected, $data->courses[0]->summary);
489         $this->assertFalse($result['responses'][1]['error']);
490         $data = json_decode($result['responses'][1]['data']);
491         $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
492         $this->assertEquals($expected, $data->courses[0]->summary);
494         $this->assertFalse($result['responses'][2]['error']);
495         $data = json_decode($result['responses'][2]['data']);
496         $this->assertEquals($course->summary, $data->courses[0]->summary);
498         $this->assertFalse($result['responses'][3]['error']);
499         $data = json_decode($result['responses'][3]['data']);
500         $expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
501         $SESSION->lang = 'en'; // We expect filtered text in english.
502         $expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
503         $this->assertEquals($expected, $data->courses[0]->summary);
504     }