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;
18
 
19
use core\oauth2\access_token;
20
use core\oauth2\api;
21
use core\oauth2\endpoint;
22
use core\oauth2\issuer;
23
use core\oauth2\system_account;
24
use \core\oauth2\user_field_mapping;
25
 
26
/**
27
 * Tests for oauth2 apis (\core\oauth2\*).
28
 *
29
 * @package    core
30
 * @copyright  2017 Damyon Wiese
31
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
32
 * @coversDefaultClass \core\oauth2\api
33
 */
34
class oauth2_test extends \advanced_testcase {
35
 
36
    /**
37
     * Tests the crud operations on oauth2 issuers.
38
     */
11 efrain 39
    public function test_create_and_delete_standard_issuers(): void {
1 efrain 40
        $this->resetAfterTest();
41
        $this->setAdminUser();
42
        api::create_standard_issuer('google');
43
        api::create_standard_issuer('facebook');
44
        api::create_standard_issuer('microsoft');
45
        api::create_standard_issuer('nextcloud', 'https://dummy.local/nextcloud/');
46
 
47
        $issuers = api::get_all_issuers();
48
 
49
        $this->assertEquals($issuers[0]->get('name'), 'Google');
50
        $this->assertEquals($issuers[1]->get('name'), 'Facebook');
51
        $this->assertEquals($issuers[2]->get('name'), 'Microsoft');
52
        $this->assertEquals($issuers[3]->get('name'), 'Nextcloud');
53
 
54
        api::move_down_issuer($issuers[0]->get('id'));
55
 
56
        $issuers = api::get_all_issuers();
57
 
58
        $this->assertEquals($issuers[0]->get('name'), 'Facebook');
59
        $this->assertEquals($issuers[1]->get('name'), 'Google');
60
        $this->assertEquals($issuers[2]->get('name'), 'Microsoft');
61
        $this->assertEquals($issuers[3]->get('name'), 'Nextcloud');
62
 
63
        api::delete_issuer($issuers[1]->get('id'));
64
 
65
        $issuers = api::get_all_issuers();
66
 
67
        $this->assertEquals($issuers[0]->get('name'), 'Facebook');
68
        $this->assertEquals($issuers[1]->get('name'), 'Microsoft');
69
        $this->assertEquals($issuers[2]->get('name'), 'Nextcloud');
70
    }
71
 
72
    /**
73
     * Tests the crud operations on oauth2 issuers.
74
     */
11 efrain 75
    public function test_create_nextcloud_without_url(): void {
1 efrain 76
        $this->resetAfterTest();
77
        $this->setAdminUser();
78
 
79
        $this->expectException(\moodle_exception::class);
80
        api::create_standard_issuer('nextcloud');
81
    }
82
 
83
    /**
84
     * Tests we can list and delete each of the persistents related to an issuer.
85
     */
11 efrain 86
    public function test_getters(): void {
1 efrain 87
        $this->resetAfterTest();
88
        $this->setAdminUser();
89
        $issuer = api::create_standard_issuer('microsoft');
90
 
91
        $same = api::get_issuer($issuer->get('id'));
92
 
93
        foreach ($same->properties_definition() as $name => $def) {
94
            $this->assertTrue($issuer->get($name) == $same->get($name));
95
        }
96
 
97
        $endpoints = api::get_endpoints($issuer);
98
        $same = api::get_endpoint($endpoints[0]->get('id'));
99
        $this->assertEquals($endpoints[0]->get('id'), $same->get('id'));
100
        $this->assertEquals($endpoints[0]->get('name'), $same->get('name'));
101
 
102
        $todelete = $endpoints[0];
103
        api::delete_endpoint($todelete->get('id'));
104
        $endpoints = api::get_endpoints($issuer);
105
        $this->assertNotEquals($endpoints[0]->get('id'), $todelete->get('id'));
106
 
107
        $userfields = api::get_user_field_mappings($issuer);
108
        $same = api::get_user_field_mapping($userfields[0]->get('id'));
109
        $this->assertEquals($userfields[0]->get('id'), $same->get('id'));
110
 
111
        $todelete = $userfields[0];
112
        api::delete_user_field_mapping($todelete->get('id'));
113
        $userfields = api::get_user_field_mappings($issuer);
114
        $this->assertNotEquals($userfields[0]->get('id'), $todelete->get('id'));
115
    }
116
 
117
    /**
118
     * Data provider for \core_oauth2_testcase::test_get_system_oauth_client().
119
     *
120
     * @return array
121
     */
122
    public function system_oauth_client_provider() {
123
        return [
124
            [
125
                (object) [
126
                    'access_token' => 'fdas...',
127
                    'token_type' => 'Bearer',
128
                    'expires_in' => '3600',
129
                    'id_token' => 'llfsd..',
130
                ], HOURSECS - 10
131
            ],
132
            [
133
                (object) [
134
                    'access_token' => 'fdas...',
135
                    'token_type' => 'Bearer',
136
                    'id_token' => 'llfsd..',
137
                ], WEEKSECS
138
            ],
139
        ];
140
    }
141
 
142
    /**
143
     * Tests we can get a logged in oauth client for a system account.
144
     *
145
     * @dataProvider system_oauth_client_provider
146
     * @param \stdClass $responsedata The response data to be mocked.
147
     * @param int $expiresin The expected expiration time.
148
     */
11 efrain 149
    public function test_get_system_oauth_client($responsedata, $expiresin): void {
1 efrain 150
        $this->resetAfterTest();
151
        $this->setAdminUser();
152
 
153
        $issuer = api::create_standard_issuer('microsoft');
154
 
155
        $requiredscopes = api::get_system_scopes_for_issuer($issuer);
156
        // Fake a system account.
157
        $data = (object) [
158
            'issuerid' => $issuer->get('id'),
159
            'refreshtoken' => 'abc',
160
            'grantedscopes' => $requiredscopes,
161
            'email' => 'sys@example.com',
162
            'username' => 'sys'
163
        ];
164
        $sys = new system_account(0, $data);
165
        $sys->create();
166
 
167
        // Fake a response with an access token.
168
        $response = json_encode($responsedata);
169
        \curl::mock_response($response);
170
        $client = api::get_system_oauth_client($issuer);
171
        $this->assertTrue($client->is_logged_in());
172
 
173
        // Check token expiry.
174
        $accesstoken = access_token::get_record(['issuerid' => $issuer->get('id')]);
175
 
176
        // Get the difference between the actual and expected expiry times.
177
        // They might differ by a couple of seconds depending on the timing when the token gets actually processed.
178
        $expiresdifference = time() + $expiresin - $accesstoken->get('expires');
179
 
180
        // Assert that the actual token expiration is more or less the same as the expected.
181
        $this->assertGreaterThanOrEqual(0, $expiresdifference);
182
        $this->assertLessThanOrEqual(3, $expiresdifference);
183
    }
184
 
185
    /**
186
     * Tests we can enable and disable an issuer.
187
     */
11 efrain 188
    public function test_enable_disable_issuer(): void {
1 efrain 189
        $this->resetAfterTest();
190
        $this->setAdminUser();
191
 
192
        $issuer = api::create_standard_issuer('microsoft');
193
 
194
        $issuerid = $issuer->get('id');
195
 
196
        api::enable_issuer($issuerid);
197
        $check = api::get_issuer($issuer->get('id'));
198
        $this->assertTrue((boolean)$check->get('enabled'));
199
 
200
        api::enable_issuer($issuerid);
201
        $check = api::get_issuer($issuer->get('id'));
202
        $this->assertTrue((boolean)$check->get('enabled'));
203
 
204
        api::disable_issuer($issuerid);
205
        $check = api::get_issuer($issuer->get('id'));
206
        $this->assertFalse((boolean)$check->get('enabled'));
207
 
208
        api::enable_issuer($issuerid);
209
        $check = api::get_issuer($issuer->get('id'));
210
        $this->assertTrue((boolean)$check->get('enabled'));
211
    }
212
 
213
    /**
214
     * Test the alloweddomains for an issuer.
215
     */
11 efrain 216
    public function test_issuer_alloweddomains(): void {
1 efrain 217
        $this->resetAfterTest();
218
        $this->setAdminUser();
219
 
220
        $issuer = api::create_standard_issuer('microsoft');
221
 
222
        $issuer->set('alloweddomains', '');
223
 
224
        // Anything is allowed when domain is empty.
225
        $this->assertTrue($issuer->is_valid_login_domain(''));
226
        $this->assertTrue($issuer->is_valid_login_domain('a@b'));
227
        $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
228
 
229
        $issuer->set('alloweddomains', 'example.com');
230
 
231
        // One domain - must match exactly - no substrings etc.
232
        $this->assertFalse($issuer->is_valid_login_domain(''));
233
        $this->assertFalse($issuer->is_valid_login_domain('a@b'));
234
        $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
235
        $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
236
 
237
        $issuer->set('alloweddomains', 'example.com,example.net');
238
        // Multiple domains - must match any exactly - no substrings etc.
239
        $this->assertFalse($issuer->is_valid_login_domain(''));
240
        $this->assertFalse($issuer->is_valid_login_domain('a@b'));
241
        $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
242
        $this->assertFalse($issuer->is_valid_login_domain('invalid@email@example.net'));
243
        $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.net'));
244
        $this->assertTrue($issuer->is_valid_login_domain('longer.example@example.com'));
245
 
246
        $issuer->set('alloweddomains', '*.example.com');
247
        // Wildcard.
248
        $this->assertFalse($issuer->is_valid_login_domain(''));
249
        $this->assertFalse($issuer->is_valid_login_domain('a@b'));
250
        $this->assertFalse($issuer->is_valid_login_domain('longer.example@example'));
251
        $this->assertFalse($issuer->is_valid_login_domain('longer.example@example.com'));
252
        $this->assertTrue($issuer->is_valid_login_domain('longer.example@sub.example.com'));
253
    }
254
 
255
    /**
256
     * Test endpoints creation for issuers.
257
     * @dataProvider create_endpoints_for_standard_issuer_provider
258
     *
259
     * @covers ::create_endpoints_for_standard_issuer
260
     *
261
     * @param string $type Issuer type to create.
262
     * @param string|null $discoveryurl Expected discovery URL or null if this endpoint doesn't exist.
263
     * @param bool $hasmappingfields True if it's expected the issuer to create has mapping fields.
264
     * @param string|null $baseurl The service URL (mandatory parameter for some issuers, such as NextCloud or IMS OBv2.1).
265
     * @param string|null $expectedexception Name of the expected expection or null if no exception will be thrown.
266
     */
267
    public function test_create_endpoints_for_standard_issuer(string $type, ?string $discoveryurl = null,
268
        bool $hasmappingfields = true, ?string $baseurl = null, ?string $expectedexception = null): void {
269
 
270
        $this->resetAfterTest();
271
 
272
        // Mark test as long because it connects with external services.
273
        if (!PHPUNIT_LONGTEST) {
274
            $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
275
        }
276
 
277
        $this->setAdminUser();
278
 
279
        // Method create_endpoints_for_standard_issuer is called internally from create_standard_issuer.
280
        if ($expectedexception) {
281
            $this->expectException($expectedexception);
282
        }
283
        $issuer = api::create_standard_issuer($type, $baseurl);
284
 
285
        // Check endpoints have been created.
286
        $endpoints = api::get_endpoints($issuer);
287
        $this->assertNotEmpty($endpoints);
288
        $this->assertNotEmpty($issuer->get('image'));
289
        // Check discovery URL.
290
        if ($discoveryurl) {
291
            $this->assertStringContainsString($discoveryurl, $issuer->get_endpoint_url('discovery'));
292
        } else {
293
            $this->assertFalse($issuer->get_endpoint_url('discovery'));
294
        }
295
        // Check userfield mappings.
296
        $userfieldmappings =api::get_user_field_mappings($issuer);
297
        if ($hasmappingfields) {
298
            $this->assertNotEmpty($userfieldmappings);
299
        } else {
300
            $this->assertEmpty($userfieldmappings);
301
        }
302
    }
303
 
304
    /**
305
     * Data provider for test_create_endpoints_for_standard_issuer.
306
     *
307
     * @return array
308
     */
309
    public function create_endpoints_for_standard_issuer_provider(): array {
310
        return [
311
            'Google' => [
312
                'type' => 'google',
313
                'discoveryurl' => '.well-known/openid-configuration',
314
            ],
315
            'Google will work too with a valid baseurl parameter' => [
316
                'type' => 'google',
317
                'discoveryurl' => '.well-known/openid-configuration',
318
                'hasmappingfields' => true,
319
                'baseurl' => 'https://accounts.google.com/',
320
            ],
321
            'IMS OBv2.1' => [
322
                'type' => 'imsobv2p1',
323
                'discoveryurl' => '.well-known/badgeconnect.json',
324
                'hasmappingfields' => false,
325
                'baseurl' => 'https://dc.imsglobal.org/',
326
            ],
327
            'IMS OBv2.1 without slash in baseurl should work too' => [
328
                'type' => 'imsobv2p1',
329
                'discoveryurl' => '.well-known/badgeconnect.json',
330
                'hasmappingfields' => false,
331
                'baseurl' => 'https://dc.imsglobal.org',
332
            ],
333
            'IMS OBv2.1 with empty baseurl should return an exception' => [
334
                'type' => 'imsobv2p1',
335
                'discoveryurl' => null,
336
                'hasmappingfields' => false,
337
                'baseurl' => null,
338
                'expectedexception' => \moodle_exception::class,
339
            ],
340
            'Microsoft' => [
341
                'type' => 'microsoft',
342
            ],
343
            'Facebook' => [
344
                'type' => 'facebook',
345
            ],
346
            'NextCloud' => [
347
                'type' => 'nextcloud',
348
                'discoveryurl' => null,
349
                'hasmappingfields' => true,
350
                'baseurl' => 'https://dummy.local/nextcloud/',
351
            ],
352
            'NextCloud with empty baseurl should return an exception' => [
353
                'type' => 'nextcloud',
354
                'discoveryurl' => null,
355
                'hasmappingfields' => true,
356
                'baseurl' => null,
357
                'expectedexception' => \moodle_exception::class,
358
            ],
359
            'Invalid type should return an exception' => [
360
                'type' => 'fictitious',
361
                'discoveryurl' => null,
362
                'hasmappingfields' => true,
363
                'baseurl' => null,
364
                'expectedexception' => \moodle_exception::class,
365
            ],
366
        ];
367
    }
368
 
369
    /**
370
     * Test for get all issuers.
371
     */
11 efrain 372
    public function test_get_all_issuers(): void {
1 efrain 373
        $this->resetAfterTest();
374
        $this->setAdminUser();
375
        $googleissuer = api::create_standard_issuer('google');
376
        api::create_standard_issuer('facebook');
377
        api::create_standard_issuer('microsoft');
378
 
379
        // Set Google issuer to be shown only on login page.
380
        $record = $googleissuer->to_record();
381
        $record->showonloginpage = $googleissuer::LOGINONLY;
382
        api::update_issuer($record);
383
 
384
        $issuers = api::get_all_issuers();
385
        $this->assertCount(2, $issuers);
386
        $expected = ['Microsoft', 'Facebook'];
387
        $this->assertEqualsCanonicalizing($expected, [$issuers[0]->get_display_name(), $issuers[1]->get_display_name()]);
388
 
389
        $issuers = api::get_all_issuers(true);
390
        $this->assertCount(3, $issuers);
391
        $expected = ['Google', 'Microsoft', 'Facebook'];
392
        $this->assertEqualsCanonicalizing($expected,
393
            [$issuers[0]->get_display_name(), $issuers[1]->get_display_name(), $issuers[2]->get_display_name()]);
394
    }
395
 
396
    /**
397
     * Test for is available for login.
398
     */
11 efrain 399
    public function test_is_available_for_login(): void {
1 efrain 400
        $this->resetAfterTest();
401
        $this->setAdminUser();
402
        $googleissuer = api::create_standard_issuer('google');
403
 
404
        // Set Google issuer to be shown only on login page.
405
        $record = $googleissuer->to_record();
406
        $record->showonloginpage = $googleissuer::LOGINONLY;
407
        api::update_issuer($record);
408
 
409
        $this->assertFalse($googleissuer->is_available_for_login());
410
 
411
        // Set a clientid and clientsecret.
412
        $googleissuer->set('clientid', 'clientid');
413
        $googleissuer->set('clientsecret', 'secret');
414
        $googleissuer->update();
415
 
416
        $this->assertTrue($googleissuer->is_available_for_login());
417
 
418
        // Set showonloginpage to service only.
419
        $googleissuer->set('showonloginpage', issuer::SERVICEONLY);
420
        $googleissuer->update();
421
 
422
        $this->assertFalse($googleissuer->is_available_for_login());
423
 
424
        // Set showonloginpage to everywhere (service and login) and disable issuer.
425
        $googleissuer->set('showonloginpage', issuer::EVERYWHERE);
426
        $googleissuer->set('enabled', 0);
427
        $googleissuer->update();
428
 
429
        $this->assertFalse($googleissuer->is_available_for_login());
430
 
431
        // Enable issuer.
432
        $googleissuer->set('enabled', 1);
433
        $googleissuer->update();
434
 
435
        $this->assertTrue($googleissuer->is_available_for_login());
436
 
437
        // Remove userinfo endpoint from issuer.
438
        $endpoint = endpoint::get_record([
439
            'issuerid' => $googleissuer->get('id'),
440
            'name' => 'userinfo_endpoint'
441
        ]);
442
        api::delete_endpoint($endpoint->get('id'));
443
 
444
        $this->assertFalse($googleissuer->is_available_for_login());
445
    }
446
 
447
    /**
448
     * Data provider for test_get_internalfield_list and test_get_internalfields.
449
     *
450
     * @return array
451
     */
452
    public function create_custom_profile_fields(): array {
453
        return [
454
            'data' =>
455
            [
456
                'given' => [
457
                    'Hobbies' => [
458
                        'shortname' => 'hobbies',
459
                        'name' => 'Hobbies',
460
                    ]
461
                ],
462
                'expected' => [
463
                    'Hobbies' => [
464
                        'shortname' => 'hobbies',
465
                        'name' => 'Hobbies',
466
                    ]
467
                ]
468
            ],
469
            [
470
                'given' => [
471
                    'Billing' => [
472
                        'shortname' => 'billingaddress',
473
                        'name' => 'Billing Address',
474
                    ],
475
                    'Payment' => [
476
                        'shortname' => 'creditcardnumber',
477
                        'name' => 'Credit Card Number',
478
                    ]
479
                ],
480
                'expected' => [
481
                    'Billing' => [
482
                        'shortname' => 'billingaddress',
483
                        'name' => 'Billing Address',
484
                    ],
485
                    'Payment' => [
486
                        'shortname' => 'creditcardnumber',
487
                        'name' => 'Credit Card Number',
488
                    ]
489
                ]
490
            ]
491
        ];
492
    }
493
 
494
    /**
495
     * Test getting the list of internal fields.
496
     *
497
     * @dataProvider create_custom_profile_fields
498
     * @covers ::get_internalfield_list
499
     * @param array $given Categories and profile fields.
500
     * @param array $expected Expected value.
501
     */
502
    public function test_get_internalfield_list(array $given, array $expected): void {
503
        $this->resetAfterTest();
504
        self::generate_custom_profile_fields($given);
505
 
506
        $userfieldmapping = new user_field_mapping();
507
        $internalfieldlist = $userfieldmapping->get_internalfield_list();
508
 
509
        foreach ($expected as $category => $value) {
510
            // Custom profile fields must exist.
511
            $this->assertNotEmpty($internalfieldlist[$category]);
512
 
513
            // Category must have the custom profile fields with expected value.
514
            $this->assertEquals(
515
                $internalfieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']],
516
                $value['name']
517
            );
518
        }
519
    }
520
 
521
    /**
522
     * Test getting the list of internal fields with flat array.
523
     *
524
     * @dataProvider create_custom_profile_fields
525
     * @covers ::get_internalfields
526
     * @param array $given Categories and profile fields.
527
     * @param array $expected Expected value.
528
     */
529
    public function test_get_internalfields(array $given, array $expected): void {
530
        $this->resetAfterTest();
531
        self::generate_custom_profile_fields($given);
532
 
533
        $userfieldmapping = new user_field_mapping();
534
        $internalfields = $userfieldmapping->get_internalfields();
535
 
536
        // Custom profile fields must exist.
537
        foreach ($expected as $category => $value) {
538
            $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $internalfields);
539
        }
540
    }
541
 
542
    /**
543
     * Test getting the list of empty external/custom profile fields.
544
     *
545
     * @covers ::get_internalfields
546
     */
547
    public function test_get_empty_internalfield_list(): void {
548
 
549
        // Get internal (profile) fields.
550
        $userfieldmapping = new user_field_mapping();
551
        $internalfieldlist = $userfieldmapping->get_internalfields();
552
 
553
        // Get user fields.
554
        $userfields = array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']);
555
 
556
        // Internal fields and user fields must exact same.
557
        $this->assertEquals($userfields, $internalfieldlist);
558
    }
559
 
560
    /**
561
     * Test getting Return the list of profile fields.
562
     *
563
     * @dataProvider create_custom_profile_fields
564
     * @covers ::get_profile_field_list
565
     * @param array $given Categories and profile fields.
566
     * @param array $expected Expected value.
567
     */
568
    public function test_get_profile_field_list(array $given, array $expected): void {
569
        $this->resetAfterTest();
570
        self::generate_custom_profile_fields($given);
571
 
572
        $profilefieldlist = get_profile_field_list();
573
 
574
        foreach ($expected as $category => $value) {
575
            $this->assertEquals(
576
                $profilefieldlist[$category][\core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname']],
577
                $value['name']
578
            );
579
        }
580
    }
581
 
582
    /**
583
     * Test getting the list of valid custom profile user fields.
584
     *
585
     * @dataProvider create_custom_profile_fields
586
     * @covers ::get_profile_field_names
587
     * @param array $given Categories and profile fields.
588
     * @param array $expected Expected value.
589
     */
590
    public function test_get_profile_field_names(array $given, array $expected): void {
591
        $this->resetAfterTest();
592
        self::generate_custom_profile_fields($given);
593
 
594
        $profilefieldnames = get_profile_field_names();
595
 
596
        // Custom profile fields must exist.
597
        foreach ($expected as $category => $value) {
598
            $this->assertContains( \core_user\fields::PROFILE_FIELD_PREFIX . $value['shortname'], $profilefieldnames);
599
        }
600
    }
601
 
602
    /**
603
     * Generate data into DB for Testing getting user fields mapping.
604
     *
605
     * @param array $given Categories and profile fields.
606
     */
607
    private function generate_custom_profile_fields(array $given): void {
608
        // Create a profile category and the profile fields.
609
        foreach ($given as $category => $value) {
610
            $customprofilefieldcategory = ['name' => $category, 'sortorder' => 1];
611
            $category = $this->getDataGenerator()->create_custom_profile_field_category($customprofilefieldcategory);
612
            $this->getDataGenerator()->create_custom_profile_field(
613
                ['shortname' => $value['shortname'],
614
                'name' => $value['name'],
615
                'categoryid' => $category->id,
616
                'required' => 1, 'visible' => 1, 'locked' => 0, 'datatype' => 'text', 'defaultdata' => null]);
617
        }
618
    }
619
 
620
}