Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / tests / api.py @ 5f28aa14

History | View | Annotate | Download (23.9 kB)

1
# Copyright 2011, 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from astakos.im.tests.common import *
35

    
36
from django.test import TestCase
37

    
38
from urllib import quote
39
from urlparse import urlparse, parse_qs
40
from xml.dom import minidom
41

    
42
import json
43

    
44
ROOT = '/astakos/api/'
45
u = lambda url: ROOT + url
46

    
47

    
48
class QuotaAPITest(TestCase):
49
    def test_0(self):
50
        client = Client()
51

    
52
        component1 = Component.objects.create(name="comp1")
53
        register.add_service(component1, "service1", "type1", [])
54
        # custom service resources
55
        resource11 = {"name": "service1.resource11",
56
                      "desc": "resource11 desc",
57
                      "service_type": "type1",
58
                      "allow_in_projects": True}
59
        r, _ = register.add_resource(resource11)
60
        register.update_resource(r, 100)
61
        resource12 = {"name": "service1.resource12",
62
                      "desc": "resource11 desc",
63
                      "service_type": "type1",
64
                      "unit": "bytes"}
65
        r, _ = register.add_resource(resource12)
66
        register.update_resource(r, 1024)
67

    
68
        # create user
69
        user = get_local_user('test@grnet.gr')
70
        quotas.qh_sync_user(user)
71

    
72
        component2 = Component.objects.create(name="comp2")
73
        register.add_service(component2, "service2", "type2", [])
74
        # create another service
75
        resource21 = {"name": "service2.resource21",
76
                      "desc": "resource11 desc",
77
                      "service_type": "type2",
78
                      "allow_in_projects": False}
79
        r, _ = register.add_resource(resource21)
80
        register.update_resource(r, 3)
81

    
82
        resource_names = [r['name'] for r in
83
                          [resource11, resource12, resource21]]
84

    
85
        # get resources
86
        r = client.get(u('resources'))
87
        self.assertEqual(r.status_code, 200)
88
        body = json.loads(r.content)
89
        for name in resource_names:
90
            assertIn(name, body)
91

    
92
        # get quota
93
        r = client.get(u('quotas'))
94
        self.assertEqual(r.status_code, 401)
95

    
96
        headers = {'HTTP_X_AUTH_TOKEN': user.auth_token}
97
        r = client.get(u('quotas/'), **headers)
98
        self.assertEqual(r.status_code, 200)
99
        body = json.loads(r.content)
100
        system_quota = body['system']
101
        assertIn('system', body)
102
        for name in resource_names:
103
            assertIn(name, system_quota)
104

    
105
        r = client.get(u('service_quotas'))
106
        self.assertEqual(r.status_code, 401)
107

    
108
        s1_headers = {'HTTP_X_AUTH_TOKEN': component1.auth_token}
109
        r = client.get(u('service_quotas'), **s1_headers)
110
        self.assertEqual(r.status_code, 200)
111
        body = json.loads(r.content)
112
        assertIn(user.uuid, body)
113

    
114
        r = client.get(u('commissions'), **s1_headers)
115
        self.assertEqual(r.status_code, 200)
116
        body = json.loads(r.content)
117
        self.assertEqual(body, [])
118

    
119
        # issue some commissions
120
        commission_request = {
121
            "force": False,
122
            "auto_accept": False,
123
            "name": "my commission",
124
            "provisions": [
125
                {
126
                    "holder": user.uuid,
127
                    "source": "system",
128
                    "resource": resource11['name'],
129
                    "quantity": 1
130
                },
131
                {
132
                    "holder": user.uuid,
133
                    "source": "system",
134
                    "resource": resource12['name'],
135
                    "quantity": 30000
136
                }]}
137

    
138
        post_data = json.dumps(commission_request)
139
        r = client.post(u('commissions'), post_data,
140
                        content_type='application/json', **s1_headers)
141
        self.assertEqual(r.status_code, 413)
142

    
143
        commission_request = {
144
            "force": False,
145
            "auto_accept": False,
146
            "name": "my commission",
147
            "provisions": [
148
                {
149
                    "holder": user.uuid,
150
                    "source": "system",
151
                    "resource": resource11['name'],
152
                    "quantity": 1
153
                },
154
                {
155
                    "holder": user.uuid,
156
                    "source": "system",
157
                    "resource": resource12['name'],
158
                    "quantity": 100
159
                }]}
160

    
161
        post_data = json.dumps(commission_request)
162
        r = client.post(u('commissions'), post_data,
163
                        content_type='application/json', **s1_headers)
164
        self.assertEqual(r.status_code, 201)
165
        body = json.loads(r.content)
166
        serial = body['serial']
167
        self.assertEqual(serial, 1)
168

    
169
        post_data = json.dumps(commission_request)
170
        r = client.post(u('commissions'), post_data,
171
                        content_type='application/json', **s1_headers)
172
        self.assertEqual(r.status_code, 201)
173
        body = json.loads(r.content)
174
        self.assertEqual(body['serial'], 2)
175

    
176
        post_data = json.dumps(commission_request)
177
        r = client.post(u('commissions'), post_data,
178
                        content_type='application/json', **s1_headers)
179
        self.assertEqual(r.status_code, 201)
180
        body = json.loads(r.content)
181
        self.assertEqual(body['serial'], 3)
182

    
183
        r = client.get(u('commissions'), **s1_headers)
184
        self.assertEqual(r.status_code, 200)
185
        body = json.loads(r.content)
186
        self.assertEqual(body, [1, 2, 3])
187

    
188
        r = client.get(u('commissions/' + str(serial)), **s1_headers)
189
        self.assertEqual(r.status_code, 200)
190
        body = json.loads(r.content)
191
        self.assertEqual(body['serial'], serial)
192
        assertIn('issue_time', body)
193
        self.assertEqual(body['provisions'], commission_request['provisions'])
194
        self.assertEqual(body['name'], commission_request['name'])
195

    
196
        r = client.get(u('service_quotas?user=' + user.uuid), **s1_headers)
197
        self.assertEqual(r.status_code, 200)
198
        body = json.loads(r.content)
199
        user_quota = body[user.uuid]
200
        system_quota = user_quota['system']
201
        r11 = system_quota[resource11['name']]
202
        self.assertEqual(r11['usage'], 3)
203
        self.assertEqual(r11['pending'], 3)
204

    
205
        # resolve pending commissions
206
        resolve_data = {
207
            "accept": [1, 3],
208
            "reject": [2, 3, 4],
209
        }
210
        post_data = json.dumps(resolve_data)
211

    
212
        r = client.post(u('commissions/action'), post_data,
213
                        content_type='application/json', **s1_headers)
214
        self.assertEqual(r.status_code, 200)
215
        body = json.loads(r.content)
216
        self.assertEqual(body['accepted'], [1])
217
        self.assertEqual(body['rejected'], [2])
218
        failed = body['failed']
219
        self.assertEqual(len(failed), 2)
220

    
221
        r = client.get(u('commissions/' + str(serial)), **s1_headers)
222
        self.assertEqual(r.status_code, 404)
223

    
224
        # auto accept
225
        commission_request = {
226
            "auto_accept": True,
227
            "name": "my commission",
228
            "provisions": [
229
                {
230
                    "holder": user.uuid,
231
                    "source": "system",
232
                    "resource": resource11['name'],
233
                    "quantity": 1
234
                },
235
                {
236
                    "holder": user.uuid,
237
                    "source": "system",
238
                    "resource": resource12['name'],
239
                    "quantity": 100
240
                }]}
241

    
242
        post_data = json.dumps(commission_request)
243
        r = client.post(u('commissions'), post_data,
244
                        content_type='application/json', **s1_headers)
245
        self.assertEqual(r.status_code, 201)
246
        body = json.loads(r.content)
247
        serial = body['serial']
248
        self.assertEqual(serial, 4)
249

    
250
        r = client.get(u('commissions/' + str(serial)), **s1_headers)
251
        self.assertEqual(r.status_code, 404)
252

    
253
        # malformed
254
        commission_request = {
255
            "auto_accept": True,
256
            "name": "my commission",
257
            "provisions": [
258
                {
259
                    "holder": user.uuid,
260
                    "source": "system",
261
                    "resource": resource11['name'],
262
                }
263
            ]}
264

    
265
        post_data = json.dumps(commission_request)
266
        r = client.post(u('commissions'), post_data,
267
                        content_type='application/json', **s1_headers)
268
        self.assertEqual(r.status_code, 400)
269

    
270
        commission_request = {
271
            "auto_accept": True,
272
            "name": "my commission",
273
            "provisions": "dummy"}
274

    
275
        post_data = json.dumps(commission_request)
276
        r = client.post(u('commissions'), post_data,
277
                        content_type='application/json', **s1_headers)
278
        self.assertEqual(r.status_code, 400)
279

    
280
        r = client.post(u('commissions'), commission_request,
281
                        content_type='application/json', **s1_headers)
282
        self.assertEqual(r.status_code, 400)
283

    
284
        # no holding
285
        commission_request = {
286
            "auto_accept": True,
287
            "name": "my commission",
288
            "provisions": [
289
                {
290
                    "holder": user.uuid,
291
                    "source": "system",
292
                    "resource": "non existent",
293
                    "quantity": 1
294
                },
295
                {
296
                    "holder": user.uuid,
297
                    "source": "system",
298
                    "resource": resource12['name'],
299
                    "quantity": 100
300
                }]}
301

    
302
        post_data = json.dumps(commission_request)
303
        r = client.post(u('commissions'), post_data,
304
                        content_type='application/json', **s1_headers)
305
        self.assertEqual(r.status_code, 404)
306

    
307
        # release
308
        commission_request = {
309
            "provisions": [
310
                {
311
                    "holder": user.uuid,
312
                    "source": "system",
313
                    "resource": resource11['name'],
314
                    "quantity": -1
315
                }
316
            ]}
317

    
318
        post_data = json.dumps(commission_request)
319
        r = client.post(u('commissions'), post_data,
320
                        content_type='application/json', **s1_headers)
321
        self.assertEqual(r.status_code, 201)
322
        body = json.loads(r.content)
323
        serial = body['serial']
324

    
325
        accept_data = {'accept': ""}
326
        post_data = json.dumps(accept_data)
327
        r = client.post(u('commissions/' + str(serial) + '/action'), post_data,
328
                        content_type='application/json', **s1_headers)
329
        self.assertEqual(r.status_code, 200)
330

    
331
        reject_data = {'reject': ""}
332
        post_data = json.dumps(accept_data)
333
        r = client.post(u('commissions/' + str(serial) + '/action'), post_data,
334
                        content_type='application/json', **s1_headers)
335
        self.assertEqual(r.status_code, 404)
336

    
337
        # force
338
        commission_request = {
339
            "force": True,
340
            "provisions": [
341
                {
342
                    "holder": user.uuid,
343
                    "source": "system",
344
                    "resource": resource11['name'],
345
                    "quantity": 100
346
                }]}
347

    
348
        post_data = json.dumps(commission_request)
349
        r = client.post(u('commissions'), post_data,
350
                        content_type='application/json', **s1_headers)
351
        self.assertEqual(r.status_code, 201)
352

    
353
        commission_request = {
354
            "force": True,
355
            "provisions": [
356
                {
357
                    "holder": user.uuid,
358
                    "source": "system",
359
                    "resource": resource11['name'],
360
                    "quantity": -200
361
                }]}
362

    
363
        post_data = json.dumps(commission_request)
364
        r = client.post(u('commissions'), post_data,
365
                        content_type='application/json', **s1_headers)
366
        self.assertEqual(r.status_code, 413)
367

    
368
        r = client.get(u('quotas'), **headers)
369
        self.assertEqual(r.status_code, 200)
370
        body = json.loads(r.content)
371
        system_quota = body['system']
372
        r11 = system_quota[resource11['name']]
373
        self.assertEqual(r11['usage'], 102)
374
        self.assertEqual(r11['pending'], 101)
375

    
376

    
377
class TokensApiTest(TestCase):
378
    def setUp(self):
379
        backend = activation_backends.get_backend()
380

    
381
        self.user1 = AstakosUser.objects.create(
382
            email='test1', email_verified=True, moderated=True,
383
            is_rejected=False)
384
        backend.activate_user(self.user1)
385
        assert self.user1.is_active is True
386

    
387
        self.user2 = AstakosUser.objects.create(
388
            email='test2', email_verified=True, moderated=True,
389
            is_rejected=False)
390
        backend.activate_user(self.user2)
391
        assert self.user2.is_active is True
392

    
393
        Service(name='service1', url='http://localhost/service1',
394
                api_url='http://localhost/api/service1',
395
                type='service1').save()
396
        Service(name='service2', url='http://localhost/service2',
397
                api_url='http://localhost/api/service2',
398
                type='service2').save()
399
        Service(name='service3', url='http://localhost/service3',
400
                api_url='http://localhost/api/service3',
401
                type='service3').save()
402

    
403
    def test_authenticate(self):
404
        client = Client()
405

    
406
        # Check not allowed method
407
        url = '/astakos/api/tokens'
408
        r = client.get(url, post_data={})
409
        self.assertEqual(r.status_code, 400)
410

    
411
        # Malformed request
412
        url = '/astakos/api/tokens'
413
        r = client.post(url, post_data={})
414
        self.assertEqual(r.status_code, 400)
415

    
416
        # Check unsupported xml input
417
        url = '/astakos/api/tokens'
418
        post_data = """
419
            <?xml version="1.0" encoding="UTF-8"?>
420
                <auth xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
421
                 xmlns="http://docs.openstack.org/identity/api/v2.0"
422
                 tenantName="%s">
423
                  <passwordCredentials username="%s" password="%s"/>
424
                </auth>""" % (self.user1.uuid, self.user1.uuid,
425
                              self.user1.auth_token)
426
        r = client.post(url, post_data, content_type='application/xml')
427
        self.assertEqual(r.status_code, 400)
428
        body = json.loads(r.content)
429
        self.assertEqual(body['badRequest']['message'],
430
                         "Unsupported Content-type: 'application/xml'")
431

    
432
        # Check malformed request: missing password
433
        url = '/astakos/api/tokens'
434
        post_data = """{"auth":{"passwordCredentials":{"username":"%s"},
435
                                "tenantName":"%s"}}""" % (
436
            self.user1.uuid, self.user1.uuid)
437
        r = client.post(url, post_data, content_type='application/json')
438
        self.assertEqual(r.status_code, 400)
439
        body = json.loads(r.content)
440
        self.assertEqual(body['badRequest']['message'],
441
                         'Malformed request')
442

    
443
        # Check malformed request: missing username
444
        url = '/astakos/api/tokens'
445
        post_data = """{"auth":{"passwordCredentials":{"password":"%s"},
446
                                "tenantName":"%s"}}""" % (
447
            self.user1.auth_token, self.user1.uuid)
448
        r = client.post(url, post_data, content_type='application/json')
449
        self.assertEqual(r.status_code, 400)
450
        body = json.loads(r.content)
451
        self.assertEqual(body['badRequest']['message'],
452
                         'Malformed request')
453

    
454
        # Check invalid pass
455
        url = '/astakos/api/tokens'
456
        post_data = """{"auth":{"passwordCredentials":{"username":"%s",
457
                                                       "password":"%s"},
458
                                "tenantName":"%s"}}""" % (
459
            self.user1.uuid, '', self.user1.uuid)
460
        r = client.post(url, post_data, content_type='application/json')
461
        self.assertEqual(r.status_code, 401)
462
        body = json.loads(r.content)
463
        self.assertEqual(body['unauthorized']['message'],
464
                         'Invalid token')
465

    
466
        # Check inconsistent pass
467
        url = '/astakos/api/tokens'
468
        post_data = """{"auth":{"passwordCredentials":{"username":"%s",
469
                                                       "password":"%s"},
470
                                "tenantName":"%s"}}""" % (
471
            self.user1.uuid, self.user2.auth_token, self.user2.uuid)
472
        r = client.post(url, post_data, content_type='application/json')
473
        self.assertEqual(r.status_code, 401)
474
        body = json.loads(r.content)
475
        self.assertEqual(body['unauthorized']['message'],
476
                         'Invalid credentials')
477

    
478
        # Check invalid json data
479
        url = '/astakos/api/tokens'
480
        r = client.post(url, "not json", content_type='application/json')
481
        self.assertEqual(r.status_code, 400)
482
        body = json.loads(r.content)
483
        self.assertEqual(body['badRequest']['message'], 'Invalid JSON data')
484

    
485
        # Check auth with token
486
        url = '/astakos/api/tokens'
487
        post_data = """{"auth":{"token": {"id":"%s"},
488
                        "tenantName":"%s"}}""" % (
489
            self.user1.auth_token, self.user1.uuid)
490
        r = client.post(url, post_data, content_type='application/json')
491
        self.assertEqual(r.status_code, 200)
492
        self.assertTrue(r['Content-Type'].startswith('application/json'))
493
        try:
494
            body = json.loads(r.content)
495
        except Exception, e:
496
            self.fail(e)
497

    
498
        # Check successful json response
499
        url = '/astakos/api/tokens'
500
        post_data = """{"auth":{"passwordCredentials":{"username":"%s",
501
                                                       "password":"%s"},
502
                                "tenantName":"%s"}}""" % (
503
            self.user1.uuid, self.user1.auth_token, self.user1.uuid)
504
        r = client.post(url, post_data, content_type='application/json')
505
        self.assertEqual(r.status_code, 200)
506
        self.assertTrue(r['Content-Type'].startswith('application/json'))
507
        try:
508
            body = json.loads(r.content)
509
        except Exception, e:
510
            self.fail(e)
511

    
512
        try:
513
            token = body['token']['id']
514
            user = body['user']['id']
515
            service_catalog = body['serviceCatalog']
516
        except KeyError:
517
            self.fail('Invalid response')
518

    
519
        self.assertEqual(token, self.user1.auth_token)
520
        self.assertEqual(user, self.user1.uuid)
521
        self.assertEqual(len(service_catalog), 3)
522

    
523
        # Check successful xml response
524
        url = '/astakos/api/tokens'
525
        headers = {'HTTP_ACCEPT': 'application/xml'}
526
        post_data = """{"auth":{"passwordCredentials":{"username":"%s",
527
                                                       "password":"%s"},
528
                                "tenantName":"%s"}}""" % (
529
            self.user1.uuid, self.user1.auth_token, self.user1.uuid)
530
        r = client.post(url, post_data, content_type='application/json',
531
                        **headers)
532
        self.assertEqual(r.status_code, 200)
533
        self.assertTrue(r['Content-Type'].startswith('application/xml'))
534
#        try:
535
#            body = minidom.parseString(r.content)
536
#        except Exception, e:
537
#            self.fail(e)
538

    
539
    def test_get_endpoints(self):
540
        client = Client()
541

    
542
        # Check in active user token
543
        inactive_user = AstakosUser.objects.create(email='test3')
544
        url = '/astakos/api/tokens/%s/endpoints' % quote(
545
            inactive_user.auth_token)
546
        r = client.get(url)
547
        self.assertEqual(r.status_code, 401)
548

    
549
        # Check invalid user token in path
550
        url = '/astakos/api/tokens/nouser/endpoints'
551
        r = client.get(url)
552
        self.assertEqual(r.status_code, 401)
553

    
554
        # Check forbidden
555
        url = '/astakos/api/tokens/%s/endpoints' % quote(self.user1.auth_token)
556
        headers = {'HTTP_X_AUTH_TOKEN': AstakosUser.objects.create(
557
            email='test4').auth_token}
558
        r = client.get(url, **headers)
559
        self.assertEqual(r.status_code, 401)
560

    
561
        # Check bad request method
562
        url = '/astakos/api/tokens/%s/endpoints' % quote(self.user1.auth_token)
563
        r = client.post(url)
564
        self.assertEqual(r.status_code, 400)
565

    
566
        # Check forbidden
567
        url = '/astakos/api/tokens/%s/endpoints' % quote(self.user1.auth_token)
568
        headers = {'HTTP_X_AUTH_TOKEN': self.user2.auth_token}
569
        r = client.get(url, **headers)
570
        self.assertEqual(r.status_code, 403)
571

    
572
        # Check belongsTo BadRequest
573
        url = '/astakos/api/tokens/%s/endpoints?belongsTo=%s' % (
574
            quote(self.user1.auth_token), quote(self.user2.uuid))
575
        headers = {'HTTP_X_AUTH_TOKEN': self.user1.auth_token}
576
        r = client.get(url, **headers)
577
        self.assertEqual(r.status_code, 400)
578

    
579
        # Check successful request
580
        url = '/astakos/api/tokens/%s/endpoints' % quote(self.user1.auth_token)
581
        headers = {'HTTP_X_AUTH_TOKEN': self.user1.auth_token}
582
        r = client.get(url, **headers)
583
        self.assertEqual(r.status_code, 200)
584
        self.assertEqual(r['Content-Type'], 'application/json; charset=UTF-8')
585
        try:
586
            body = json.loads(r.content)
587
        except:
588
            self.fail('json format expected')
589
        endpoints = body.get('endpoints')
590
        self.assertEqual(len(endpoints), 3)
591

    
592
         # Check xml serialization
593
        url = '/astakos/api/tokens/%s/endpoints?format=xml' %\
594
            quote(self.user1.auth_token)
595
        headers = {'HTTP_X_AUTH_TOKEN': self.user1.auth_token}
596
        r = client.get(url, **headers)
597
        self.assertEqual(r.status_code, 200)
598
        self.assertEqual(r['Content-Type'], 'application/xml; charset=UTF-8')
599
#        try:
600
#            body = minidom.parseString(r.content)
601
#        except Exception, e:
602
#            self.fail('xml format expected')
603
        endpoints = body.get('endpoints')
604
        self.assertEqual(len(endpoints), 3)
605

    
606
        # Check limit
607
        url = '/astakos/api/tokens/%s/endpoints?limit=2' %\
608
            quote(self.user1.auth_token)
609
        headers = {'HTTP_X_AUTH_TOKEN': self.user1.auth_token}
610
        r = client.get(url, **headers)
611
        self.assertEqual(r.status_code, 200)
612
        body = json.loads(r.content)
613
        endpoints = body.get('endpoints')
614
        self.assertEqual(len(endpoints), 2)
615

    
616
        endpoint_link = body.get('endpoint_links', [])[0]
617
        next = endpoint_link.get('href')
618
        p = urlparse(next)
619
        params = parse_qs(p.query)
620
        self.assertTrue('limit' in params)
621
        self.assertTrue('marker' in params)
622
        self.assertEqual(params['marker'][0], '2')
623

    
624
        # Check marker
625
        headers = {'HTTP_X_AUTH_TOKEN': self.user1.auth_token}
626
        r = client.get(next, **headers)
627
        self.assertEqual(r.status_code, 200)
628
        body = json.loads(r.content)
629
        endpoints = body.get('endpoints')
630
        self.assertEqual(len(endpoints), 1)