~gandelman-a/ubuntu/precise/keystone/UCA_2012.2.1

« back to all changes in this revision

Viewing changes to tests/test_auth_token_middleware.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short, Adam Gandelman, Soren Hansen, Logan Rosen, Chuck Short
  • Date: 2012-09-07 13:04:01 UTC
  • mfrom: (1.1.22)
  • Revision ID: package-import@ubuntu.com-20120907130401-o49wh9xxkr2cmuqx
Tags: 2012.2~rc1~20120906.2517-0ubuntu2
[ Adam Gandelman ]
* Refreshed patches.

[ Soren Hansen ]
* Update debian/watch to account for symbolically named tarballs and
  use newer URL.
* Fix Launchpad URLs in debian/watch.

[ Logan Rosen ]
* Fix control file to suggest python-memcache instead of python-memcached
  (LP: #998991).

[ Chuck Short ]
* New upstream version.
* Dont FTBFS if the testsuite fails.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
import datetime
18
18
import iso8601
 
19
import os
 
20
import string
 
21
import tempfile
 
22
 
19
23
import webob
20
24
 
 
25
from keystone.common import cms
 
26
from keystone.common import utils
21
27
from keystone.middleware import auth_token
22
28
from keystone.openstack.common import jsonutils
23
 
from keystone import config
 
29
from keystone.openstack.common import timeutils
24
30
from keystone import test
25
31
 
26
32
 
 
33
REVOCATION_LIST = None
 
34
REVOKED_TOKEN = None
 
35
REVOKED_TOKEN_HASH = None
 
36
SIGNED_REVOCATION_LIST = None
 
37
SIGNED_TOKEN_SCOPED = None
 
38
SIGNED_TOKEN_UNSCOPED = None
 
39
VALID_SIGNED_REVOCATION_LIST = None
 
40
 
 
41
UUID_TOKEN_DEFAULT = "ec6c0710ec2f471498484c1b53ab4f9d"
 
42
UUID_TOKEN_NO_SERVICE_CATALOG = '8286720fbe4941e69fa8241723bb02df'
 
43
UUID_TOKEN_UNSCOPED = '731f903721c14827be7b2dc912af7776'
 
44
VALID_DIABLO_TOKEN = 'b0cf19b55dbb4f20a6ee18e6c6cf1726'
 
45
 
 
46
INVALID_SIGNED_TOKEN = string.replace(
 
47
    """AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 
48
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
 
49
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
 
50
DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
 
51
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
 
52
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
 
53
0000000000000000000000000000000000000000000000000000000000000000
 
54
1111111111111111111111111111111111111111111111111111111111111111
 
55
2222222222222222222222222222222222222222222222222222222222222222
 
56
3333333333333333333333333333333333333333333333333333333333333333
 
57
4444444444444444444444444444444444444444444444444444444444444444
 
58
5555555555555555555555555555555555555555555555555555555555555555
 
59
6666666666666666666666666666666666666666666666666666666666666666
 
60
7777777777777777777777777777777777777777777777777777777777777777
 
61
8888888888888888888888888888888888888888888888888888888888888888
 
62
9999999999999999999999999999999999999999999999999999999999999999
 
63
0000000000000000000000000000000000000000000000000000000000000000
 
64
xg==""", "\n", "")
 
65
 
27
66
# JSON responses keyed by token ID
28
67
TOKEN_RESPONSES = {
29
 
    'valid-token': {
 
68
    UUID_TOKEN_DEFAULT: {
30
69
        'access': {
31
70
            'token': {
32
 
                'id': 'valid-token',
 
71
                'id': UUID_TOKEN_DEFAULT,
33
72
                'tenant': {
34
73
                    'id': 'tenant_id1',
35
74
                    'name': 'tenant_name1',
46
85
            'serviceCatalog': {}
47
86
        },
48
87
    },
49
 
    'default-tenant-token': {
50
 
        'access': {
51
 
            'token': {
52
 
                'id': 'default-tenant-token',
53
 
            },
54
 
            'user': {
55
 
                'id': 'user_id1',
56
 
                'name': 'user_name1',
57
 
                'tenantId': 'tenant_id1',
58
 
                'tenantName': 'tenant_name1',
59
 
                'roles': [
60
 
                    {'name': 'role1'},
61
 
                    {'name': 'role2'},
62
 
                ],
63
 
            },
64
 
        },
65
 
    },
66
 
    'valid-diablo-token': {
67
 
        'access': {
68
 
            'token': {
69
 
                'id': 'valid-diablo-token',
70
 
                'tenantId': 'tenant_id1',
71
 
            },
72
 
            'user': {
73
 
                'id': 'user_id1',
74
 
                'name': 'user_name1',
75
 
                'roles': [
76
 
                    {'name': 'role1'},
77
 
                    {'name': 'role2'},
78
 
                ],
79
 
            },
80
 
        },
81
 
    },
82
 
    'unscoped-token': {
83
 
        'access': {
84
 
            'token': {
85
 
                'id': 'unscoped-token',
86
 
            },
87
 
            'user': {
88
 
                'id': 'user_id1',
89
 
                'name': 'user_name1',
90
 
                'roles': [
91
 
                    {'name': 'role1'},
92
 
                    {'name': 'role2'},
93
 
                ],
94
 
            },
95
 
        },
96
 
    },
97
 
    'valid-token-no-service-catalog': {
 
88
    VALID_DIABLO_TOKEN: {
 
89
        'access': {
 
90
            'token': {
 
91
                'id': VALID_DIABLO_TOKEN,
 
92
                'tenantId': 'tenant_id1',
 
93
            },
 
94
            'user': {
 
95
                'id': 'user_id1',
 
96
                'name': 'user_name1',
 
97
                'roles': [
 
98
                    {'name': 'role1'},
 
99
                    {'name': 'role2'},
 
100
                ],
 
101
            },
 
102
        },
 
103
    },
 
104
    UUID_TOKEN_UNSCOPED: {
 
105
        'access': {
 
106
            'token': {
 
107
                'id': UUID_TOKEN_UNSCOPED,
 
108
            },
 
109
            'user': {
 
110
                'id': 'user_id1',
 
111
                'name': 'user_name1',
 
112
                'roles': [
 
113
                    {'name': 'role1'},
 
114
                    {'name': 'role2'},
 
115
                ],
 
116
            },
 
117
        },
 
118
    },
 
119
    UUID_TOKEN_NO_SERVICE_CATALOG: {
98
120
        'access': {
99
121
            'token': {
100
122
                'id': 'valid-token',
116
138
}
117
139
 
118
140
 
 
141
# The data for these tests are signed using openssl and are stored in files
 
142
# in the signing subdirectory.  In order to keep the values consistent between
 
143
# the tests and the signed documents, we read them in for use in the tests.
 
144
def setUpModule(self):
 
145
    signing_path = os.path.join(os.path.dirname(__file__), 'signing')
 
146
    with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f:
 
147
        self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read())
 
148
    with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f:
 
149
        self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read())
 
150
    with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f:
 
151
        self.REVOKED_TOKEN = cms.cms_to_token(f.read())
 
152
    self.REVOKED_TOKEN_HASH = utils.hash_signed_token(self.REVOKED_TOKEN)
 
153
    with open(os.path.join(signing_path, 'revocation_list.json')) as f:
 
154
        self.REVOCATION_LIST = jsonutils.loads(f.read())
 
155
    with open(os.path.join(signing_path, 'revocation_list.pem')) as f:
 
156
        self.VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps(
 
157
            {'signed': f.read()})
 
158
 
 
159
    self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED] = {
 
160
        'access': {
 
161
            'token': {
 
162
                'id': self.SIGNED_TOKEN_SCOPED,
 
163
            },
 
164
            'user': {
 
165
                'id': 'user_id1',
 
166
                'name': 'user_name1',
 
167
                'tenantId': 'tenant_id1',
 
168
                'tenantName': 'tenant_name1',
 
169
                'roles': [
 
170
                    {'name': 'role1'},
 
171
                    {'name': 'role2'},
 
172
                ],
 
173
            },
 
174
        },
 
175
    }
 
176
 
 
177
    self.TOKEN_RESPONSES[self.SIGNED_TOKEN_UNSCOPED] = {
 
178
        'access': {
 
179
            'token': {
 
180
                'id': self.SIGNED_TOKEN_UNSCOPED,
 
181
            },
 
182
            'user': {
 
183
                'id': 'user_id1',
 
184
                'name': 'user_name1',
 
185
                'roles': [
 
186
                    {'name': 'role1'},
 
187
                    {'name': 'role2'},
 
188
                ],
 
189
            },
 
190
        },
 
191
    },
 
192
 
 
193
 
119
194
class FakeMemcache(object):
120
195
    def __init__(self):
121
196
        self.set_key = None
123
198
        self.token_expiration = None
124
199
 
125
200
    def get(self, key):
126
 
        data = TOKEN_RESPONSES['valid-token'].copy()
 
201
        data = TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED].copy()
127
202
        if not data or key != "tokens/%s" % (data['access']['token']['id']):
128
203
            return
129
204
        if not self.token_expiration:
153
228
    last_requested_url = ''
154
229
 
155
230
    def __init__(self, *args):
156
 
        pass
 
231
        self.send_valid_revocation_list = True
157
232
 
158
233
    def request(self, method, path, **kwargs):
159
234
        """Fakes out several http responses.
180
255
            if token_id in TOKEN_RESPONSES.keys():
181
256
                status = 200
182
257
                body = jsonutils.dumps(TOKEN_RESPONSES[token_id])
 
258
            elif token_id == "revoked":
 
259
                status = 200
 
260
                body = SIGNED_REVOCATION_LIST
183
261
            else:
184
262
                status = 404
185
263
                body = str()
220
298
 
221
299
 
222
300
class BaseAuthTokenMiddlewareTest(test.TestCase):
 
301
 
223
302
    def setUp(self, expected_env=None):
224
303
        expected_env = expected_env or {}
225
304
 
228
307
            'auth_host': 'keystone.example.com',
229
308
            'auth_port': 1234,
230
309
            'auth_admin_prefix': '/testadmin',
 
310
            'signing_dir': 'signing',
231
311
        }
232
312
 
233
313
        self.middleware = auth_token.AuthProtocol(FakeApp(expected_env), conf)
236
316
 
237
317
        self.response_status = None
238
318
        self.response_headers = None
 
319
        self.middleware.revoked_file_name = tempfile.mkstemp()[1]
 
320
        self.middleware.token_revocation_list_cache_timeout =\
 
321
            datetime.timedelta(days=1)
 
322
        self.middleware.token_revocation_list = jsonutils.dumps(
 
323
            {"revoked": [], "extra": "success"})
 
324
 
 
325
        globals()['SIGNED_REVOCATION_LIST'] =\
 
326
            globals()['VALID_SIGNED_REVOCATION_LIST']
 
327
 
239
328
        super(BaseAuthTokenMiddlewareTest, self).setUp()
240
329
 
 
330
    def tearDown(self):
 
331
        super(BaseAuthTokenMiddlewareTest, self).tearDown()
 
332
        try:
 
333
            os.remove(self.middleware.revoked_file_name)
 
334
        except OSError:
 
335
            pass
 
336
 
241
337
    def start_fake_response(self, status, headers):
242
338
        self.response_status = int(status.split(' ', 1)[0])
243
339
        self.response_headers = dict(headers)
250
346
        expected_env = {
251
347
            'HTTP_X_TENANT_ID': 'tenant_id1',
252
348
            'HTTP_X_TENANT_NAME': 'tenant_id1',
253
 
            'HTTP_X_TENANT': 'tenant_id1',  # now deprecated (diablo-compat)
 
349
            # now deprecated (diablo-compat)
 
350
            'HTTP_X_TENANT': 'tenant_id1',
254
351
        }
255
352
        super(DiabloAuthTokenMiddlewareTest, self).setUp(expected_env)
256
353
 
257
 
    def test_diablo_response(self):
 
354
    def test_valid_diablo_response(self):
258
355
        req = webob.Request.blank('/')
259
 
        req.headers['X-Auth-Token'] = 'valid-diablo-token'
260
 
        body = self.middleware(req.environ, self.start_fake_response)
 
356
        req.headers['X-Auth-Token'] = VALID_DIABLO_TOKEN
 
357
        self.middleware(req.environ, self.start_fake_response)
261
358
        self.assertEqual(self.response_status, 200)
262
 
        self.assertEqual(body, ['SUCCESS'])
263
359
 
264
360
 
265
361
class AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest):
266
 
    def test_valid_request(self):
 
362
    def assert_valid_request_200(self, token):
267
363
        req = webob.Request.blank('/')
268
 
        req.headers['X-Auth-Token'] = 'valid-token'
 
364
        req.headers['X-Auth-Token'] = token
269
365
        body = self.middleware(req.environ, self.start_fake_response)
 
366
        self.assertEqual(self.response_status, 200)
 
367
        self.assertTrue(req.headers.get('X-Service-Catalog'))
 
368
        self.assertEqual(body, ['SUCCESS'])
 
369
 
 
370
    def test_valid_uuid_request(self):
 
371
        self.assert_valid_request_200(UUID_TOKEN_DEFAULT)
 
372
        self.assertEqual("/testadmin/v2.0/tokens/%s" % UUID_TOKEN_DEFAULT,
 
373
                         FakeHTTPConnection.last_requested_url)
 
374
 
 
375
    def test_valid_signed_request(self):
 
376
        FakeHTTPConnection.last_requested_url = ''
 
377
        self.assert_valid_request_200(SIGNED_TOKEN_SCOPED)
270
378
        self.assertEqual(self.middleware.conf['auth_admin_prefix'],
271
379
                         "/testadmin")
272
 
        self.assertEqual("/testadmin/v2.0/tokens/valid-token",
273
 
                         FakeHTTPConnection.last_requested_url)
274
 
        self.assertEqual(self.response_status, 200)
275
 
        self.assertTrue(req.headers.get('X-Service-Catalog'))
276
 
        self.assertEqual(body, ['SUCCESS'])
 
380
        #ensure that signed requests do not generate HTTP traffic
 
381
        self.assertEqual('', FakeHTTPConnection.last_requested_url)
277
382
 
278
 
    def test_default_tenant_token(self):
 
383
    def assert_unscoped_default_tenant_auto_scopes(self, token):
279
384
        """Unscoped requests with a default tenant should "auto-scope."
280
385
 
281
386
        The implied scope is the user's tenant ID.
282
387
 
283
388
        """
284
389
        req = webob.Request.blank('/')
285
 
        req.headers['X-Auth-Token'] = 'default-tenant-token'
 
390
        req.headers['X-Auth-Token'] = token
286
391
        body = self.middleware(req.environ, self.start_fake_response)
287
392
        self.assertEqual(self.response_status, 200)
288
393
        self.assertEqual(body, ['SUCCESS'])
289
394
 
290
 
    def test_unscoped_token(self):
 
395
    def test_default_tenant_uuid_token(self):
 
396
        self.assert_unscoped_default_tenant_auto_scopes(UUID_TOKEN_DEFAULT)
 
397
 
 
398
    def test_default_tenant_signed_token(self):
 
399
        self.assert_unscoped_default_tenant_auto_scopes(SIGNED_TOKEN_SCOPED)
 
400
 
 
401
    def assert_unscoped_token_receives_401(self, token):
291
402
        """Unscoped requests with no default tenant ID should be rejected."""
292
403
        req = webob.Request.blank('/')
293
 
        req.headers['X-Auth-Token'] = 'unscoped-token'
 
404
        req.headers['X-Auth-Token'] = token
294
405
        self.middleware(req.environ, self.start_fake_response)
295
406
        self.assertEqual(self.response_status, 401)
296
407
        self.assertEqual(self.response_headers['WWW-Authenticate'],
297
408
                         'Keystone uri=\'https://keystone.example.com:1234\'')
298
409
 
299
 
    def test_request_invalid_token(self):
 
410
    def test_unscoped_uuid_token_receives_401(self):
 
411
        self.assert_unscoped_token_receives_401(UUID_TOKEN_UNSCOPED)
 
412
 
 
413
    def test_unscoped_pki_token_receives_401(self):
 
414
        self.assert_unscoped_token_receives_401(SIGNED_TOKEN_UNSCOPED)
 
415
 
 
416
    def test_revoked_token_receives_401(self):
 
417
        self.middleware.token_revocation_list = self.get_revocation_list_json()
 
418
        req = webob.Request.blank('/')
 
419
        req.headers['X-Auth-Token'] = REVOKED_TOKEN
 
420
        self.middleware(req.environ, self.start_fake_response)
 
421
        self.assertEqual(self.response_status, 401)
 
422
 
 
423
    def get_revocation_list_json(self, token_ids=None):
 
424
        if token_ids is None:
 
425
            token_ids = [REVOKED_TOKEN_HASH]
 
426
        revocation_list = {'revoked': [{'id': x, 'expires': timeutils.utcnow()}
 
427
                                       for x in token_ids]}
 
428
        return jsonutils.dumps(revocation_list)
 
429
 
 
430
    def test_is_signed_token_revoked_returns_false(self):
 
431
        #explicitly setting an empty revocation list here to document intent
 
432
        self.middleware.token_revocation_list = jsonutils.dumps(
 
433
            {"revoked": [], "extra": "success"})
 
434
        result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN)
 
435
        self.assertFalse(result)
 
436
 
 
437
    def test_is_signed_token_revoked_returns_true(self):
 
438
        self.middleware.token_revocation_list = self.get_revocation_list_json()
 
439
        result = self.middleware.is_signed_token_revoked(REVOKED_TOKEN)
 
440
        self.assertTrue(result)
 
441
 
 
442
    def test_verify_signed_token_raises_exception_for_revoked_token(self):
 
443
        self.middleware.token_revocation_list = self.get_revocation_list_json()
 
444
        with self.assertRaises(auth_token.InvalidUserToken):
 
445
            self.middleware.verify_signed_token(REVOKED_TOKEN)
 
446
 
 
447
    def test_verify_signed_token_succeeds_for_unrevoked_token(self):
 
448
        self.middleware.token_revocation_list = self.get_revocation_list_json()
 
449
        self.middleware.verify_signed_token(SIGNED_TOKEN_SCOPED)
 
450
 
 
451
    def test_get_token_revocation_list_fetched_time_returns_min(self):
 
452
        self.middleware.token_revocation_list_fetched_time = None
 
453
        self.middleware.revoked_file_name = ''
 
454
        self.assertEqual(self.middleware.token_revocation_list_fetched_time,
 
455
                         datetime.datetime.min)
 
456
 
 
457
    def test_get_token_revocation_list_fetched_time_returns_mtime(self):
 
458
        self.middleware.token_revocation_list_fetched_time = None
 
459
        mtime = os.path.getmtime(self.middleware.revoked_file_name)
 
460
        fetched_time = datetime.datetime.fromtimestamp(mtime)
 
461
        self.assertEqual(self.middleware.token_revocation_list_fetched_time,
 
462
                         fetched_time)
 
463
 
 
464
    def test_get_token_revocation_list_fetched_time_returns_value(self):
 
465
        expected = self.middleware._token_revocation_list_fetched_time
 
466
        self.assertEqual(self.middleware.token_revocation_list_fetched_time,
 
467
                         expected)
 
468
 
 
469
    def test_get_revocation_list_returns_fetched_list(self):
 
470
        self.middleware.token_revocation_list_fetched_time = None
 
471
        os.remove(self.middleware.revoked_file_name)
 
472
        self.assertEqual(self.middleware.token_revocation_list,
 
473
                         REVOCATION_LIST)
 
474
 
 
475
    def test_get_revocation_list_returns_current_list_from_memory(self):
 
476
        self.assertEqual(self.middleware.token_revocation_list,
 
477
                         self.middleware._token_revocation_list)
 
478
 
 
479
    def test_get_revocation_list_returns_current_list_from_disk(self):
 
480
        in_memory_list = self.middleware.token_revocation_list
 
481
        self.middleware._token_revocation_list = None
 
482
        self.assertEqual(self.middleware.token_revocation_list, in_memory_list)
 
483
 
 
484
    def test_invalid_revocation_list_raises_service_error(self):
 
485
        globals()['SIGNED_REVOCATION_LIST'] = "{}"
 
486
        with self.assertRaises(auth_token.ServiceError):
 
487
            self.middleware.fetch_revocation_list()
 
488
 
 
489
    def test_fetch_revocation_list(self):
 
490
        fetched_list = jsonutils.loads(self.middleware.fetch_revocation_list())
 
491
        self.assertEqual(fetched_list, REVOCATION_LIST)
 
492
 
 
493
    def test_request_invalid_uuid_token(self):
300
494
        req = webob.Request.blank('/')
301
495
        req.headers['X-Auth-Token'] = 'invalid-token'
302
496
        self.middleware(req.environ, self.start_fake_response)
304
498
        self.assertEqual(self.response_headers['WWW-Authenticate'],
305
499
                         'Keystone uri=\'https://keystone.example.com:1234\'')
306
500
 
 
501
    def test_request_invalid_signed_token(self):
 
502
        req = webob.Request.blank('/')
 
503
        req.headers['X-Auth-Token'] = INVALID_SIGNED_TOKEN
 
504
        self.middleware(req.environ, self.start_fake_response)
 
505
        self.assertEqual(self.response_status, 401)
 
506
        self.assertEqual(self.response_headers['WWW-Authenticate'],
 
507
                         'Keystone uri=\'https://keystone.example.com:1234\'')
 
508
 
307
509
    def test_request_no_token(self):
308
510
        req = webob.Request.blank('/')
309
511
        self.middleware(req.environ, self.start_fake_response)
321
523
 
322
524
    def test_memcache(self):
323
525
        req = webob.Request.blank('/')
324
 
        req.headers['X-Auth-Token'] = 'valid-token'
 
526
        req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED
325
527
        self.middleware._cache = FakeMemcache()
326
528
        self.middleware(req.environ, self.start_fake_response)
327
529
        self.assertEqual(self.middleware._cache.set_value, None)
335
537
 
336
538
    def test_memcache_set_expired(self):
337
539
        req = webob.Request.blank('/')
338
 
        req.headers['X-Auth-Token'] = 'valid-token'
 
540
        req.headers['X-Auth-Token'] = SIGNED_TOKEN_SCOPED
339
541
        self.middleware._cache = FakeMemcache()
340
542
        expired = datetime.datetime.now() - datetime.timedelta(minutes=1)
341
543
        self.middleware._cache.token_expiration = float(expired.strftime("%s"))
357
559
    def test_request_prevent_service_catalog_injection(self):
358
560
        req = webob.Request.blank('/')
359
561
        req.headers['X-Service-Catalog'] = '[]'
360
 
        req.headers['X-Auth-Token'] = 'valid-token-no-service-catalog'
 
562
        req.headers['X-Auth-Token'] = UUID_TOKEN_NO_SERVICE_CATALOG
361
563
        body = self.middleware(req.environ, self.start_fake_response)
362
564
        self.assertEqual(self.response_status, 200)
363
565
        self.assertFalse(req.headers.get('X-Service-Catalog'))
364
566
        self.assertEqual(body, ['SUCCESS'])
365
 
 
366
 
 
367
 
if __name__ == '__main__':
368
 
    import unittest
369
 
    unittest.main()