MDL-63102 core_block: Reduced spacing between blocks
[moodle.git] / admin / tool / dataprivacy / tests / external_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  * External tests.
19  *
20  * @package    tool_dataprivacy
21  * @copyright  2018 Jun Pataleta
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
26 global $CFG;
28 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
30 use tool_dataprivacy\api;
31 use tool_dataprivacy\context_instance;
32 use tool_dataprivacy\external;
34 /**
35  * External testcase.
36  *
37  * @package    tool_dataprivacy
38  * @copyright  2018 Jun Pataleta
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
43     /** @var stdClass The user making the request. */
44     protected $requester;
46     /** @var int The data request ID. */
47     protected $requestid;
49     /**
50      * Setup function- we will create a course and add an assign instance to it.
51      */
52     protected function setUp() {
53         $this->resetAfterTest();
55         $generator = new testing_data_generator();
56         $requester = $generator->create_user();
58         $comment = 'sample comment';
60         // Login as user.
61         $this->setUser($requester->id);
63         // Test data request creation.
64         $datarequest = api::create_data_request($requester->id, api::DATAREQUEST_TYPE_EXPORT, $comment);
65         $this->requestid = $datarequest->get('id');
66         $this->requester = $requester;
68         // Log out the user and set force login to true.
69         $this->setUser();
70     }
72     /**
73      * Test for external::approve_data_request() with the user not logged in.
74      */
75     public function test_approve_data_request_not_logged_in() {
76         $this->expectException(require_login_exception::class);
77         external::approve_data_request($this->requestid);
78     }
80     /**
81      * Test for external::approve_data_request() with the user not having a DPO role.
82      */
83     public function test_approve_data_request_not_dpo() {
84         // Login as the requester.
85         $this->setUser($this->requester->id);
86         $this->expectException(required_capability_exception::class);
87         external::approve_data_request($this->requestid);
88     }
90     /**
91      * Test for external::approve_data_request() for request that's not ready for approval
92      */
93     public function test_approve_data_request_not_waiting_for_approval() {
94         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
95         $this->setAdminUser();
96         $this->expectException(moodle_exception::class);
97         external::approve_data_request($this->requestid);
98     }
100     /**
101      * Test for external::approve_data_request()
102      */
103     public function test_approve_data_request() {
104         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
105         $this->setAdminUser();
106         api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
107         $result = external::approve_data_request($this->requestid);
108         $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
109         $this->assertTrue($return->result);
110         $this->assertEmpty($return->warnings);
111     }
113     /**
114      * Test for external::approve_data_request() for a non-existent request ID.
115      */
116     public function test_approve_data_request_non_existent() {
117         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
118         $this->setAdminUser();
119         api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
120         $result = external::approve_data_request($this->requestid + 1);
121         $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
122         $this->assertFalse($return->result);
123         $this->assertCount(1, $return->warnings);
124         $warning = reset($return->warnings);
125         $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
126     }
128     /**
129      * Test for external::cancel_data_request() of another user.
130      */
131     public function test_cancel_data_request_other_user() {
132         $generator = $this->getDataGenerator();
133         $otheruser = $generator->create_user();
135         // Login as another user.
136         $this->setUser($otheruser);
138         $result = external::cancel_data_request($this->requestid);
139         $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
140         $this->assertFalse($return->result);
141         $this->assertCount(1, $return->warnings);
142         $warning = reset($return->warnings);
143         $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
144     }
146     /**
147      * Test for external::cancel_data_request()
148      */
149     public function test_cancel_data_request() {
150         // Login as the requester.
151         $this->setUser($this->requester);
153         $result = external::cancel_data_request($this->requestid);
154         $return = (object) external_api::clean_returnvalue(external::approve_data_request_returns(), $result);
155         $this->assertTrue($return->result);
156         $this->assertEmpty($return->warnings);
157     }
159     /**
160      * Test contact DPO.
161      */
162     public function test_contact_dpo() {
163         $generator = $this->getDataGenerator();
164         $user = $generator->create_user();
165         $this->setUser($user);
167         $message = 'Hello world!';
168         $result = external::contact_dpo($message);
169         $return = (object) external_api::clean_returnvalue(external::contact_dpo_returns(), $result);
170         $this->assertTrue($return->result);
171         $this->assertEmpty($return->warnings);
172     }
174     /**
175      * Test contact DPO with message containing invalid input.
176      */
177     public function test_contact_dpo_with_nasty_input() {
178         $generator = $this->getDataGenerator();
179         $user = $generator->create_user();
180         $this->setUser($user);
182         $this->expectException('invalid_parameter_exception');
183         external::contact_dpo('de<>\\..scription');
184     }
186     /**
187      * Test for external::deny_data_request() with the user not logged in.
188      */
189     public function test_deny_data_request_not_logged_in() {
190         $this->expectException(require_login_exception::class);
191         external::deny_data_request($this->requestid);
192     }
194     /**
195      * Test for external::deny_data_request() with the user not having a DPO role.
196      */
197     public function test_deny_data_request_not_dpo() {
198         // Login as the requester.
199         $this->setUser($this->requester->id);
200         $this->expectException(required_capability_exception::class);
201         external::deny_data_request($this->requestid);
202     }
204     /**
205      * Test for external::deny_data_request() for request that's not ready for approval
206      */
207     public function test_deny_data_request_not_waiting_for_approval() {
208         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
209         $this->setAdminUser();
210         $this->expectException(moodle_exception::class);
211         external::deny_data_request($this->requestid);
212     }
214     /**
215      * Test for external::deny_data_request()
216      */
217     public function test_deny_data_request() {
218         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
219         $this->setAdminUser();
220         api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
221         $result = external::approve_data_request($this->requestid);
222         $return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
223         $this->assertTrue($return->result);
224         $this->assertEmpty($return->warnings);
225     }
227     /**
228      * Test for external::deny_data_request() for a non-existent request ID.
229      */
230     public function test_deny_data_request_non_existent() {
231         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
232         $this->setAdminUser();
233         api::update_request_status($this->requestid, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
234         $result = external::deny_data_request($this->requestid + 1);
235         $return = (object) external_api::clean_returnvalue(external::deny_data_request_returns(), $result);
236         $this->assertFalse($return->result);
237         $this->assertCount(1, $return->warnings);
238         $warning = reset($return->warnings);
239         $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
240     }
242     /**
243      * Test for external::get_data_request() with the user not logged in.
244      */
245     public function test_get_data_request_not_logged_in() {
246         $this->expectException(require_login_exception::class);
247         external::get_data_request($this->requestid);
248     }
250     /**
251      * Test for external::get_data_request() with the user not having a DPO role.
252      */
253     public function test_get_data_request_not_dpo() {
254         $generator = $this->getDataGenerator();
255         $otheruser = $generator->create_user();
256         // Login as the requester.
257         $this->setUser($otheruser);
258         $this->expectException(required_capability_exception::class);
259         external::get_data_request($this->requestid);
260     }
262     /**
263      * Test for external::get_data_request()
264      */
265     public function test_get_data_request() {
266         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
267         $this->setAdminUser();
268         $result = external::get_data_request($this->requestid);
269         $return = (object) external_api::clean_returnvalue(external::get_data_request_returns(), $result);
270         $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $return->result['type']);
271         $this->assertEquals('sample comment', $return->result['comments']);
272         $this->assertEquals($this->requester->id, $return->result['userid']);
273         $this->assertEquals($this->requester->id, $return->result['requestedby']);
274         $this->assertEmpty($return->warnings);
275     }
277     /**
278      * Test for external::get_data_request() for a non-existent request ID.
279      */
280     public function test_get_data_request_non_existent() {
281         // Admin as DPO. (The default when no one's assigned as a DPO in the site).
282         $this->setAdminUser();
283         $this->expectException(dml_missing_record_exception::class);
284         external::get_data_request($this->requestid + 1);
285     }
287     /**
288      * Test for \tool_dataprivacy\external::set_context_defaults()
289      * when called by a user that doesn't have the manage registry capability.
290      */
291     public function test_set_context_defaults_no_capability() {
292         $generator = $this->getDataGenerator();
293         $user = $generator->create_user();
294         $this->setUser($user);
295         $this->expectException(required_capability_exception::class);
296         external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false);
297     }
299     /**
300      * Test for \tool_dataprivacy\external::set_context_defaults().
301      *
302      * We're just checking the module context level here to test the WS function.
303      * More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults().
304      *
305      * @dataProvider get_options_provider
306      * @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only.
307      * @param bool $override Whether to override instances.
308      */
309     public function test_set_context_defaults($modulelevel, $override) {
310         $this->setAdminUser();
311         $generator = $this->getDataGenerator();
313         // Generate course cat, course, block, assignment, forum instances.
314         $coursecat = $generator->create_category();
315         $course = $generator->create_course(['category' => $coursecat->id]);
316         $assign = $generator->create_module('assign', ['course' => $course->id]);
317         list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
318         $assigncontext = context_module::instance($assigncm->id);
320         // Generate purpose and category.
321         $category1 = api::create_category((object)['name' => 'Test category 1']);
322         $category2 = api::create_category((object)['name' => 'Test category 2']);
323         $purpose1 = api::create_purpose((object)[
324             'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
325         ]);
326         $purpose2 = api::create_purpose((object)[
327             'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
328         ]);
330         // Set a custom purpose and ID for this assignment instance.
331         $assignctxinstance = api::set_context_instance((object) [
332             'contextid' => $assigncontext->id,
333             'purposeid' => $purpose1->get('id'),
334             'categoryid' => $category1->get('id'),
335         ]);
337         $modulename = $modulelevel ? 'assign' : '';
338         $categoryid = $category2->get('id');
339         $purposeid = $purpose2->get('id');
340         $result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override);
342         // Extract the result.
343         $return = external_api::clean_returnvalue(external::set_context_defaults_returns(), $result);
344         $this->assertTrue($return['result']);
346         // Check the assignment context instance.
347         $instanceexists = context_instance::record_exists($assignctxinstance->get('id'));
348         if ($override) {
349             // The custom assign instance should have been deleted.
350             $this->assertFalse($instanceexists);
351         } else {
352             // The custom assign instance should still exist.
353             $this->assertTrue($instanceexists);
354         }
356         // Check the saved defaults.
357         list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename);
358         $this->assertEquals($categoryid, $savedcategory);
359         $this->assertEquals($purposeid, $savedpurpose);
360     }
362     /**
363      * Test for \tool_dataprivacy\external::get_category_options()
364      * when called by a user that doesn't have the manage registry capability.
365      */
366     public function test_get_category_options_no_capability() {
367         $generator = $this->getDataGenerator();
368         $user = $generator->create_user();
369         $this->setUser($user);
370         $this->expectException(required_capability_exception::class);
371         external::get_category_options(true, true);
372     }
374     /**
375      * Data provider for \tool_dataprivacy_external_testcase::test_XX_options().
376      */
377     public function get_options_provider() {
378         return [
379             [false, false],
380             [false, true],
381             [true, false],
382             [true, true],
383         ];
384     }
386     /**
387      * Test for \tool_dataprivacy\external::get_category_options().
388      *
389      * @dataProvider get_options_provider
390      * @param bool $includeinherit Whether "Inherit" would be included to the options.
391      * @param bool $includenotset Whether "Not set" would be included to the options.
392      */
393     public function test_get_category_options($includeinherit, $includenotset) {
394         $this->setAdminUser();
396         // Prepare our expected options.
397         $expectedoptions = [];
398         if ($includeinherit) {
399             $expectedoptions[] = [
400                 'id' => context_instance::INHERIT,
401                 'name' => get_string('inherit', 'tool_dataprivacy'),
402             ];
403         }
405         if ($includenotset) {
406             $expectedoptions[] = [
407                 'id' => context_instance::NOTSET,
408                 'name' => get_string('notset', 'tool_dataprivacy'),
409             ];
410         }
412         for ($i = 1; $i <= 3; $i++) {
413             $category = api::create_category((object)['name' => 'Category ' . $i]);
414             $expectedoptions[] = [
415                 'id' => $category->get('id'),
416                 'name' => $category->get('name'),
417             ];
418         }
420         // Call the WS function.
421         $result = external::get_category_options($includeinherit, $includenotset);
423         // Extract the options.
424         $return = (object) external_api::clean_returnvalue(external::get_category_options_returns(), $result);
425         $options = $return->options;
427         // Make sure everything checks out.
428         $this->assertCount(count($expectedoptions), $options);
429         foreach ($options as $option) {
430             $this->assertContains($option, $expectedoptions);
431         }
432     }
434     /**
435      * Test for \tool_dataprivacy\external::get_purpose_options()
436      * when called by a user that doesn't have the manage registry capability.
437      */
438     public function test_get_purpose_options_no_capability() {
439         $generator = $this->getDataGenerator();
440         $user = $generator->create_user();
441         $this->setUser($user);
442         $this->expectException(required_capability_exception::class);
443         external::get_category_options(true, true);
444     }
446     /**
447      * Test for \tool_dataprivacy\external::get_purpose_options().
448      *
449      * @dataProvider get_options_provider
450      * @param bool $includeinherit Whether "Inherit" would be included to the options.
451      * @param bool $includenotset Whether "Not set" would be included to the options.
452      */
453     public function test_get_purpose_options($includeinherit, $includenotset) {
454         $this->setAdminUser();
456         // Prepare our expected options.
457         $expectedoptions = [];
458         if ($includeinherit) {
459             $expectedoptions[] = [
460                 'id' => context_instance::INHERIT,
461                 'name' => get_string('inherit', 'tool_dataprivacy'),
462             ];
463         }
465         if ($includenotset) {
466             $expectedoptions[] = [
467                 'id' => context_instance::NOTSET,
468                 'name' => get_string('notset', 'tool_dataprivacy'),
469             ];
470         }
472         for ($i = 1; $i <= 3; $i++) {
473             $purpose = api::create_purpose((object)[
474                 'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
475             ]);
476             $expectedoptions[] = [
477                 'id' => $purpose->get('id'),
478                 'name' => $purpose->get('name'),
479             ];
480         }
482         // Call the WS function.
483         $result = external::get_purpose_options($includeinherit, $includenotset);
485         // Extract the options.
486         $return = (object) external_api::clean_returnvalue(external::get_purpose_options_returns(), $result);
487         $options = $return->options;
489         // Make sure everything checks out.
490         $this->assertCount(count($expectedoptions), $options);
491         foreach ($options as $option) {
492             $this->assertContains($option, $expectedoptions);
493         }
494     }
496     /**
497      * Data provider for \tool_dataprivacy_external_testcase::get_activity_options().
498      */
499     public function get_activity_options_provider() {
500         return [
501             [false, false, true],
502             [false, true, true],
503             [true, false, true],
504             [true, true, true],
505             [false, false, false],
506             [false, true, false],
507             [true, false, false],
508             [true, true, false],
509         ];
510     }
512     /**
513      * Test for \tool_dataprivacy\external::get_activity_options().
514      *
515      * @dataProvider get_activity_options_provider
516      * @param bool $inheritcategory Whether the category would be set to "Inherit".
517      * @param bool $inheritpurpose Whether the purpose would be set to "Inherit".
518      * @param bool $nodefaults Whether to fetch only activities that don't have defaults.
519      */
520     public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
521         $this->setAdminUser();
523         $category = api::create_category((object)['name' => 'Test category']);
524         $purpose = api::create_purpose((object)[
525             'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
526         ]);
527         $categoryid = $category->get('id');
528         $purposeid = $purpose->get('id');
530         if ($inheritcategory) {
531             $categoryid = context_instance::INHERIT;
532         }
533         if ($inheritpurpose) {
534             $purposeid = context_instance::INHERIT;
535         }
537         // Set the context default for the assignment module.
538         api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign');
540         // Call the WS function.
541         $result = external::get_activity_options($nodefaults);
543         // Extract the options.
544         $return = (object) external_api::clean_returnvalue(external::get_activity_options_returns(), $result);
545         $options = $return->options;
547         // Make sure the options list is not empty.
548         $this->assertNotEmpty($options);
550         $pluginwithdefaults = [
551             'name' => 'assign',
552             'displayname' => get_string('pluginname', 'assign')
553         ];
555         // If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit,
556         // the assign module should be listed.
557         if (!$nodefaults || ($inheritcategory && $inheritpurpose)) {
558             $this->assertContains($pluginwithdefaults, $options);
559         } else {
560             $this->assertNotContains($pluginwithdefaults, $options);
561         }
562     }
564     /**
565      * Test for external::bulk_approve_data_requests().
566      */
567     public function test_bulk_approve_data_requests() {
568         $generator = new testing_data_generator();
569         $requester1 = $generator->create_user();
570         $comment1 = 'sample comment';
571         // Login as requester2.
572         $this->setUser($requester1->id);
573         // Create delete data request.
574         $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
576         $requestid1 = $datarequest1->get('id');
577         $requestid2 = $this->requestid;
579         $this->setAdminUser();
580         api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
581         api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
582         $result = external::bulk_approve_data_requests([$requestid1, $requestid2]);
583         $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
584         $this->assertTrue($return->result);
585         $this->assertEmpty($return->warnings);
586     }
588     /**
589      * Test for external::bulk_approve_data_requests() for a non-existent request ID.
590      */
591     public function test_bulk_approve_data_requests_non_existent() {
592         $generator = new testing_data_generator();
593         $requester1 = $generator->create_user();
594         $comment1 = 'sample comment';
595         // Login as requester2.
596         $this->setUser($requester1->id);
597         // Create delete data request.
598         $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
600         $requestid1 = $datarequest1->get('id');
601         $requestid2 = $this->requestid;
603         $this->setAdminUser();
604         api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
605         api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
606         $result = external::bulk_approve_data_requests([$requestid1 + 1, $requestid2]);
607         $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
608         $this->assertFalse($return->result);
609         $this->assertCount(1, $return->warnings);
610         $warning = reset($return->warnings);
611         $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
612         $this->assertEquals($requestid1 + 1, $warning['item']);
613     }
615     /**
616      * Test for external::bulk_deny_data_requests().
617      */
618     public function test_bulk_deny_data_requests() {
619         $generator = new testing_data_generator();
620         $requester1 = $generator->create_user();
621         $comment1 = 'sample comment';
622         // Login as requester2.
623         $this->setUser($requester1->id);
624         // Create delete data request.
625         $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
627         $requestid1 = $datarequest1->get('id');
628         $requestid2 = $this->requestid;
630         $this->setAdminUser();
631         api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
632         api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
633         $result = external::bulk_deny_data_requests([$requestid1, $requestid2]);
634         $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
635         $this->assertTrue($return->result);
636         $this->assertEmpty($return->warnings);
637     }
639     /**
640      * Test for external::bulk_deny_data_requests() for a non-existent request ID.
641      */
642     public function test_bulk_deny_data_requests_non_existent() {
643         $generator = new testing_data_generator();
644         $requester1 = $generator->create_user();
645         $comment1 = 'sample comment';
646         // Login as requester2.
647         $this->setUser($requester1->id);
648         // Create delete data request.
649         $datarequest1 = api::create_data_request($requester1->id, api::DATAREQUEST_TYPE_DELETE, $comment1);
651         $requestid1 = $datarequest1->get('id');
652         $requestid2 = $this->requestid;
654         $this->setAdminUser();
655         api::update_request_status($requestid1, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
656         api::update_request_status($requestid2, api::DATAREQUEST_STATUS_AWAITING_APPROVAL);
657         $result = external::bulk_deny_data_requests([$requestid1 + 1, $requestid2]);
658         $return = (object) external_api::clean_returnvalue(external::bulk_approve_data_requests_returns(), $result);
659         $this->assertFalse($return->result);
660         $this->assertCount(1, $return->warnings);
661         $warning = reset($return->warnings);
662         $this->assertEquals('errorrequestnotfound', $warning['warningcode']);
663         $this->assertEquals($requestid1 + 1, $warning['item']);
664     }