django-esi 8.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. django_esi-8.1.0.dist-info/METADATA +93 -0
  2. django_esi-8.1.0.dist-info/RECORD +100 -0
  3. django_esi-8.1.0.dist-info/WHEEL +4 -0
  4. django_esi-8.1.0.dist-info/licenses/LICENSE +674 -0
  5. esi/__init__.py +7 -0
  6. esi/admin.py +42 -0
  7. esi/aiopenapi3/client.py +79 -0
  8. esi/aiopenapi3/plugins.py +224 -0
  9. esi/app_settings.py +112 -0
  10. esi/apps.py +11 -0
  11. esi/checks.py +56 -0
  12. esi/clients.py +657 -0
  13. esi/decorators.py +271 -0
  14. esi/errors.py +22 -0
  15. esi/exceptions.py +51 -0
  16. esi/helpers.py +63 -0
  17. esi/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  18. esi/locale/cs_CZ/LC_MESSAGES/django.po +53 -0
  19. esi/locale/de/LC_MESSAGES/django.mo +0 -0
  20. esi/locale/de/LC_MESSAGES/django.po +58 -0
  21. esi/locale/en/LC_MESSAGES/django.mo +0 -0
  22. esi/locale/en/LC_MESSAGES/django.po +54 -0
  23. esi/locale/es/LC_MESSAGES/django.mo +0 -0
  24. esi/locale/es/LC_MESSAGES/django.po +59 -0
  25. esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
  26. esi/locale/fr_FR/LC_MESSAGES/django.po +59 -0
  27. esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
  28. esi/locale/it_IT/LC_MESSAGES/django.po +59 -0
  29. esi/locale/ja/LC_MESSAGES/django.mo +0 -0
  30. esi/locale/ja/LC_MESSAGES/django.po +58 -0
  31. esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
  32. esi/locale/ko_KR/LC_MESSAGES/django.po +58 -0
  33. esi/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
  34. esi/locale/nl_NL/LC_MESSAGES/django.po +53 -0
  35. esi/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  36. esi/locale/pl_PL/LC_MESSAGES/django.po +53 -0
  37. esi/locale/ru/LC_MESSAGES/django.mo +0 -0
  38. esi/locale/ru/LC_MESSAGES/django.po +61 -0
  39. esi/locale/sk/LC_MESSAGES/django.mo +0 -0
  40. esi/locale/sk/LC_MESSAGES/django.po +55 -0
  41. esi/locale/uk/LC_MESSAGES/django.mo +0 -0
  42. esi/locale/uk/LC_MESSAGES/django.po +57 -0
  43. esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  44. esi/locale/zh_Hans/LC_MESSAGES/django.po +58 -0
  45. esi/management/commands/__init__.py +0 -0
  46. esi/management/commands/esi_clear_spec_cache.py +21 -0
  47. esi/management/commands/generate_esi_stubs.py +661 -0
  48. esi/management/commands/migrate_to_ssov2.py +188 -0
  49. esi/managers.py +303 -0
  50. esi/managers.pyi +85 -0
  51. esi/migrations/0001_initial.py +55 -0
  52. esi/migrations/0002_scopes_20161208.py +56 -0
  53. esi/migrations/0003_hide_tokens_from_admin_site.py +23 -0
  54. esi/migrations/0004_remove_unique_access_token.py +18 -0
  55. esi/migrations/0005_remove_token_length_limit.py +23 -0
  56. esi/migrations/0006_remove_url_length_limit.py +18 -0
  57. esi/migrations/0007_fix_mysql_8_migration.py +18 -0
  58. esi/migrations/0008_nullable_refresh_token.py +18 -0
  59. esi/migrations/0009_set_old_tokens_to_sso_v1.py +18 -0
  60. esi/migrations/0010_set_new_tokens_to_sso_v2.py +18 -0
  61. esi/migrations/0011_add_token_indices.py +28 -0
  62. esi/migrations/0012_fix_token_type_choices.py +18 -0
  63. esi/migrations/0013_squashed_0012_fix_token_type_choices.py +57 -0
  64. esi/migrations/__init__.py +0 -0
  65. esi/models.py +349 -0
  66. esi/openapi_clients.py +1225 -0
  67. esi/rate_limiting.py +107 -0
  68. esi/signals.py +21 -0
  69. esi/static/esi/img/EVE_SSO_Login_Buttons_Large_Black.png +0 -0
  70. esi/static/esi/img/EVE_SSO_Login_Buttons_Large_White.png +0 -0
  71. esi/static/esi/img/EVE_SSO_Login_Buttons_Small_Black.png +0 -0
  72. esi/static/esi/img/EVE_SSO_Login_Buttons_Small_White.png +0 -0
  73. esi/stubs.py +2 -0
  74. esi/stubs.pyi +6807 -0
  75. esi/tasks.py +78 -0
  76. esi/templates/esi/select_token.html +116 -0
  77. esi/templatetags/__init__.py +0 -0
  78. esi/templatetags/scope_tags.py +8 -0
  79. esi/tests/__init__.py +134 -0
  80. esi/tests/client_authed_pilot.py +63 -0
  81. esi/tests/client_public_pilot.py +53 -0
  82. esi/tests/factories.py +47 -0
  83. esi/tests/factories_2.py +60 -0
  84. esi/tests/jwt_factory.py +135 -0
  85. esi/tests/test_checks.py +48 -0
  86. esi/tests/test_clients.py +1019 -0
  87. esi/tests/test_decorators.py +578 -0
  88. esi/tests/test_management_command.py +307 -0
  89. esi/tests/test_managers.py +673 -0
  90. esi/tests/test_models.py +403 -0
  91. esi/tests/test_openapi.json +854 -0
  92. esi/tests/test_openapi.py +1017 -0
  93. esi/tests/test_swagger.json +489 -0
  94. esi/tests/test_swagger_full.json +51112 -0
  95. esi/tests/test_tasks.py +116 -0
  96. esi/tests/test_templatetags.py +22 -0
  97. esi/tests/test_views.py +331 -0
  98. esi/tests/threading_pilot.py +69 -0
  99. esi/urls.py +9 -0
  100. esi/views.py +129 -0
@@ -0,0 +1,578 @@
1
+ """unit tests for esi decorators"""
2
+
3
+ import logging
4
+ from time import time
5
+ from unittest.mock import patch, Mock
6
+
7
+ from django.core.cache import cache
8
+ from django.contrib.auth.models import User
9
+ from django.contrib.auth.views import redirect_to_login
10
+ from django.contrib.sessions.middleware import SessionMiddleware
11
+ from django.http import HttpResponse
12
+ from django.test import TestCase, RequestFactory
13
+
14
+ from . import _generate_token, _store_as_Token
15
+ from ..rate_limiting import ESIRateLimitBucket, ESIRateLimits
16
+ from ..exceptions import ESIBucketLimitException
17
+ from ..decorators import (
18
+ _check_callback, esi_rate_limiter_bucketed, tokens_required, token_required, single_use_token, wait_for_esi_errorlimit_reset
19
+ )
20
+ from ..models import Token, CallbackRedirect
21
+
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class TestCheckCallback(TestCase):
27
+
28
+ def setUp(self):
29
+ self.user = User.objects.create_user(
30
+ 'Bruce Wayne',
31
+ 'abc@example.com',
32
+ 'password'
33
+ )
34
+
35
+ self.token = _store_as_Token(
36
+ _generate_token(
37
+ character_id=99,
38
+ character_name=self.user.username,
39
+ scopes=['abc', 'xyz', '123']
40
+ ),
41
+ self.user
42
+ )
43
+ self.factory = RequestFactory()
44
+ CallbackRedirect.objects.all().delete()
45
+
46
+ def test_normal(self):
47
+ logger.debug('start')
48
+ request = self.factory.get('https://www.example.com/callback2/')
49
+ request.user = self.user
50
+ middleware = SessionMiddleware(HttpResponse)
51
+ middleware.process_request(request)
52
+ request.session.save()
53
+
54
+ CallbackRedirect.objects.create(
55
+ session_key=request.session.session_key,
56
+ url='https://www.example.com/redirect/',
57
+ state='xyz',
58
+ token=self.token
59
+ )
60
+ response = _check_callback(request)
61
+ self.assertEqual(response, self.token)
62
+
63
+ def test_no_cb(self):
64
+ logger.debug('start')
65
+ request = self.factory.get('https://www.example.com/callback2/')
66
+ request.user = self.user
67
+ middleware = SessionMiddleware(HttpResponse)
68
+ middleware.process_request(request)
69
+ request.session.save()
70
+
71
+ response = _check_callback(request)
72
+ self.assertIsNone(response)
73
+
74
+ def test_no_token(self):
75
+ logger.debug('start')
76
+ request = self.factory.get('https://www.example.com/callback2/')
77
+ request.user = self.user
78
+ middleware = SessionMiddleware(HttpResponse)
79
+ middleware.process_request(request)
80
+ request.session.save()
81
+
82
+ CallbackRedirect.objects.create(
83
+ session_key=request.session.session_key,
84
+ url='https://www.example.com/redirect/',
85
+ state='xyz'
86
+ )
87
+
88
+ response = _check_callback(request)
89
+ self.assertIsNone(response)
90
+
91
+ def test_no_session(self):
92
+ logger.debug('start')
93
+ request = self.factory.get('https://www.example.com/callback2/')
94
+ request.user = self.user
95
+ middleware = SessionMiddleware(HttpResponse)
96
+ middleware.process_request(request)
97
+
98
+ response = _check_callback(request)
99
+ self.assertIsNone(response)
100
+
101
+
102
+ class TestTokensRequired(TestCase):
103
+
104
+ def setUp(self):
105
+ self.user = User.objects.create_user(
106
+ 'Bruce Wayne',
107
+ 'abc@example.com',
108
+ 'password'
109
+ )
110
+
111
+ self.token = _store_as_Token(
112
+ _generate_token(
113
+ character_id=99,
114
+ character_name=self.user.username,
115
+ scopes=['abc', '123']
116
+ ),
117
+ self.user
118
+ )
119
+ self.factory = RequestFactory()
120
+ CallbackRedirect.objects.all().delete()
121
+
122
+ def test_token_already_exists(self):
123
+ logger.debug('start')
124
+
125
+ @tokens_required(scopes='abc')
126
+ def my_view(request, tokens):
127
+ return tokens
128
+
129
+ request = self.factory.get('https://www.example.com/my_view/')
130
+ request.user = self.user
131
+ middleware = SessionMiddleware(HttpResponse)
132
+ middleware.process_request(request)
133
+ request.session.save()
134
+
135
+ response = my_view(request)
136
+ self.assertEqual(
137
+ response.first(),
138
+ self.token
139
+ )
140
+
141
+ def test_token_and_cb_already_exists(self):
142
+ logger.debug('start')
143
+
144
+ @tokens_required(scopes='abc')
145
+ def my_view(request, tokens):
146
+ return tokens
147
+
148
+ request = self.factory.get('https://www.example.com/my_view/')
149
+ request.user = self.user
150
+ middleware = SessionMiddleware(HttpResponse)
151
+ middleware.process_request(request)
152
+ request.session.save()
153
+
154
+ CallbackRedirect.objects.create(
155
+ session_key=request.session.session_key,
156
+ url='https://www.example.com/redirect/',
157
+ state='xyz',
158
+ token=self.token
159
+ )
160
+
161
+ response = my_view(request)
162
+ self.assertEqual(
163
+ response.first(),
164
+ self.token
165
+ )
166
+
167
+ @patch('esi.views.sso_redirect', autospec=True)
168
+ def test_token_does_not_exist(self, mock_sso_redirect):
169
+ logger.debug('start')
170
+
171
+ @tokens_required(scopes='xyz')
172
+ def my_view(request, tokens):
173
+ return tokens
174
+
175
+ mock_sso_redirect.return_value = 'sso_redirect_view_called'
176
+
177
+ request = self.factory.get('https://www.example.com/my_view/')
178
+ request.user = self.user
179
+ middleware = SessionMiddleware(HttpResponse)
180
+ middleware.process_request(request)
181
+ request.session.save()
182
+
183
+ response = my_view(request)
184
+ self.assertEqual(
185
+ response,
186
+ 'sso_redirect_view_called'
187
+ )
188
+
189
+ def test_coming_back_from_sso(self):
190
+ logger.debug('start')
191
+
192
+ @tokens_required(scopes='abc', new=True)
193
+ def my_view(request, tokens):
194
+ return tokens
195
+
196
+ request = self.factory.get('https://www.example.com/my_view/')
197
+ request.user = self.user
198
+ middleware = SessionMiddleware(HttpResponse)
199
+ middleware.process_request(request)
200
+ request.session.save()
201
+
202
+ CallbackRedirect.objects.create(
203
+ session_key=request.session.session_key,
204
+ url='https://www.example.com/redirect/',
205
+ state='xyz',
206
+ token=self.token
207
+ )
208
+
209
+ response = my_view(request)
210
+ self.assertEqual(
211
+ response.first(),
212
+ self.token
213
+ )
214
+
215
+ def test_user_not_authed(self):
216
+ logger.debug('start')
217
+
218
+ @tokens_required(scopes='abc')
219
+ def my_view(request, tokens):
220
+ return tokens
221
+
222
+ mock_user = Mock(spec=User)
223
+ mock_user.is_authenticated = False
224
+
225
+ request = self.factory.get('https://www.example.com/my_view/')
226
+ request.user = mock_user
227
+ middleware = SessionMiddleware(HttpResponse)
228
+ middleware.process_request(request)
229
+ request.session.save()
230
+
231
+ response = my_view(request)
232
+ self.assertEqual(
233
+ response.url,
234
+ redirect_to_login(request.get_full_path()).url
235
+ )
236
+
237
+
238
+ class TestTokenRequired(TestCase):
239
+
240
+ def setUp(self):
241
+ self.user = User.objects.create_user(
242
+ 'Bruce Wayne',
243
+ 'abc@example.com',
244
+ 'password'
245
+ )
246
+
247
+ self.token = _store_as_Token(
248
+ _generate_token(
249
+ character_id=99,
250
+ character_name=self.user.username,
251
+ scopes=['abc', '123']
252
+ ),
253
+ self.user
254
+ )
255
+ self.factory = RequestFactory()
256
+ CallbackRedirect.objects.all().delete()
257
+
258
+ @patch('esi.views.select_token', autospec=True)
259
+ @patch('esi.views.sso_redirect', autospec=True)
260
+ def test_initial_call_with_matching_tokens(
261
+ self,
262
+ mock_sso_redirect,
263
+ mock_select_token
264
+ ):
265
+ logger.debug('start')
266
+
267
+ @token_required(scopes='abc')
268
+ def my_view(request, tokens):
269
+ return tokens
270
+
271
+ mock_sso_redirect.return_value = 'sso_redirect_view_called'
272
+ mock_select_token.return_value = 'select_token_view_called'
273
+
274
+ request = self.factory.get('https://www.example.com/my_view/')
275
+ request.user = self.user
276
+ middleware = SessionMiddleware(HttpResponse)
277
+ middleware.process_request(request)
278
+ request.session.save()
279
+
280
+ response = my_view(request)
281
+ self.assertEqual(
282
+ response,
283
+ 'select_token_view_called'
284
+ )
285
+
286
+ @patch('esi.views.select_token', autospec=True)
287
+ @patch('esi.views.sso_redirect', autospec=True)
288
+ def test_initial_call_wo_matching_tokens(
289
+ self,
290
+ mock_sso_redirect,
291
+ mock_select_token
292
+ ):
293
+ logger.debug('start')
294
+
295
+ @token_required(scopes='xyz')
296
+ def my_view(request, tokens):
297
+ return tokens
298
+
299
+ mock_sso_redirect.return_value = 'sso_redirect_view_called'
300
+ mock_select_token.return_value = 'select_token_view_called'
301
+
302
+ request = self.factory.get('https://www.example.com/my_view/')
303
+ request.user = self.user
304
+ middleware = SessionMiddleware(HttpResponse)
305
+ middleware.process_request(request)
306
+ request.session.save()
307
+
308
+ response = my_view(request)
309
+ self.assertEqual(
310
+ response,
311
+ 'sso_redirect_view_called'
312
+ )
313
+
314
+ def test_coming_back_from_sso_normal(
315
+ self,
316
+ ):
317
+ logger.debug('start')
318
+
319
+ @token_required(scopes='abc', new=True)
320
+ def my_view(request, token):
321
+ return token
322
+
323
+ request = self.factory.get('https://www.example.com/my_view/')
324
+ request.user = self.user
325
+ middleware = SessionMiddleware(HttpResponse)
326
+ middleware.process_request(request)
327
+ request.session.save()
328
+
329
+ CallbackRedirect.objects.create(
330
+ session_key=request.session.session_key,
331
+ url='https://www.example.com/redirect/',
332
+ state='qwe123',
333
+ token=self.token
334
+ )
335
+
336
+ response = my_view(request)
337
+ self.assertEqual(
338
+ response,
339
+ self.token
340
+ )
341
+
342
+ @patch('esi.views.select_token', autospec=True)
343
+ @patch('esi.views.sso_redirect', autospec=True)
344
+ def test_user_wants_to_add_new_token(
345
+ self,
346
+ mock_sso_redirect,
347
+ mock_select_token
348
+ ):
349
+ logger.debug('start')
350
+
351
+ @token_required(scopes='abc')
352
+ def my_view(request, token):
353
+ return token
354
+
355
+ mock_sso_redirect.return_value = 'sso_redirect_view_called'
356
+ mock_select_token.return_value = 'select_token_view_called'
357
+
358
+ request = self.factory.post(
359
+ 'https://www.example.com/my_view/',
360
+ {
361
+ '_add': False
362
+ }
363
+ )
364
+ request.user = self.user
365
+ middleware = SessionMiddleware(HttpResponse)
366
+ middleware.process_request(request)
367
+ request.session.save()
368
+
369
+ CallbackRedirect.objects.create(
370
+ session_key=request.session.session_key,
371
+ url='https://www.example.com/redirect/',
372
+ state='qwe123',
373
+ token=self.token
374
+ )
375
+
376
+ response = my_view(request)
377
+ self.assertEqual(
378
+ response,
379
+ self.token
380
+ )
381
+
382
+ @patch('esi.views.select_token', autospec=True)
383
+ @patch('esi.views.sso_redirect', autospec=True)
384
+ def test_user_chose_existing_token(
385
+ self,
386
+ mock_sso_redirect,
387
+ mock_select_token
388
+ ):
389
+ logger.debug('start')
390
+
391
+ @token_required(scopes='abc')
392
+ def my_view(request, token):
393
+ return token
394
+
395
+ mock_sso_redirect.return_value = 'sso_redirect_view_called'
396
+ mock_select_token.return_value = 'select_token_view_called'
397
+
398
+ request = self.factory.post(
399
+ 'https://www.example.com/my_view/',
400
+ {
401
+ '_token': self.token.pk
402
+ }
403
+ )
404
+ request.user = self.user
405
+ middleware = SessionMiddleware(HttpResponse)
406
+ middleware.process_request(request)
407
+ request.session.save()
408
+
409
+ CallbackRedirect.objects.create(
410
+ session_key=request.session.session_key,
411
+ url='https://www.example.com/redirect/',
412
+ state='qwe123',
413
+ token=self.token
414
+ )
415
+
416
+ response = my_view(request)
417
+ self.assertEqual(
418
+ response,
419
+ self.token
420
+ )
421
+
422
+ @patch('esi.views.select_token', autospec=True)
423
+ @patch('esi.views.sso_redirect', autospec=True)
424
+ def test_user_chose_token_which_does_not_exist(
425
+ self,
426
+ mock_sso_redirect,
427
+ mock_select_token
428
+ ):
429
+ logger.debug('start')
430
+
431
+ @token_required(scopes='abc')
432
+ def my_view(request, token):
433
+ return token
434
+
435
+ mock_sso_redirect.return_value = 'sso_redirect_view_called'
436
+ mock_select_token.return_value = 'select_token_view_called'
437
+
438
+ invalid_token_pk = max(int(x.pk) for x in Token.objects.all()) + 1
439
+ request = self.factory.post(
440
+ 'https://www.example.com/my_view/',
441
+ {
442
+ '_token': invalid_token_pk
443
+ }
444
+ )
445
+ request.user = self.user
446
+ middleware = SessionMiddleware(HttpResponse)
447
+ middleware.process_request(request)
448
+ request.session.save()
449
+
450
+ response = my_view(request)
451
+ self.assertEqual(
452
+ response,
453
+ 'select_token_view_called'
454
+ )
455
+
456
+
457
+ class TestSingleUseTokenRequired(TestCase):
458
+
459
+ def setUp(self):
460
+ self.token = _store_as_Token(
461
+ _generate_token(
462
+ character_id=99,
463
+ character_name="No Auth Character",
464
+ scopes=['abc', '123']
465
+ ),
466
+ None
467
+ )
468
+ self.factory = RequestFactory()
469
+ CallbackRedirect.objects.all().delete()
470
+
471
+ @patch('esi.views.sso_redirect', autospec=True)
472
+ def test_initial_call_wo_matching_tokens(
473
+ self,
474
+ mock_sso_redirect
475
+ ):
476
+ logger.debug('start')
477
+
478
+ @single_use_token(scopes='xyz')
479
+ def my_view(request, tokens):
480
+ return tokens
481
+
482
+ mock_sso_redirect.return_value = 'sso_redirect_view_called'
483
+
484
+ request = self.factory.get('https://www.example.com/my_view/')
485
+ request.user = None
486
+ middleware = SessionMiddleware(HttpResponse)
487
+ middleware.process_request(request)
488
+ request.session.save()
489
+
490
+ response = my_view(request)
491
+ self.assertEqual(
492
+ response,
493
+ 'sso_redirect_view_called'
494
+ )
495
+
496
+ def test_coming_back_from_sso_normal(
497
+ self,
498
+ ):
499
+ logger.debug('start')
500
+
501
+ @single_use_token(scopes='abc', new=True)
502
+ def my_view(request, token):
503
+ return token
504
+
505
+ request = self.factory.get('https://www.example.com/my_view/')
506
+ request.user = None
507
+ middleware = SessionMiddleware(HttpResponse)
508
+ middleware.process_request(request)
509
+ request.session.save()
510
+
511
+ CallbackRedirect.objects.create(
512
+ session_key=request.session.session_key,
513
+ url='https://www.example.com/redirect/',
514
+ state='qwe123',
515
+ token=self.token
516
+ )
517
+
518
+ response = my_view(request)
519
+ self.assertEqual(
520
+ response,
521
+ self.token
522
+ )
523
+
524
+
525
+ class TestESIRateLimitDecorator(TestCase):
526
+
527
+ def setUp(self):
528
+ self.bucket = ESIRateLimitBucket(
529
+ "test-bucket",
530
+ 1,
531
+ 5
532
+ )
533
+ cache.clear()
534
+
535
+ def test_raise(self):
536
+ @esi_rate_limiter_bucketed(bucket=self.bucket)
537
+ def my_func():
538
+ return "Pass"
539
+
540
+ _t = time()
541
+ my_func()
542
+ self.assertLess(time() - _t, 1)
543
+ _t = time()
544
+ with self.assertRaises(ESIBucketLimitException):
545
+ my_func()
546
+
547
+ def test_sleep(self):
548
+ @esi_rate_limiter_bucketed(bucket=self.bucket, raise_on_limit=False)
549
+ def my_func():
550
+ return "Pass"
551
+
552
+ _t = time()
553
+ my_func()
554
+ self.assertLess(time() - _t, 1)
555
+ _t = time()
556
+ my_func()
557
+ duration = time() - _t
558
+ self.assertGreater(duration, 5)
559
+
560
+
561
+ class TestESIErrorLimitDecorator(TestCase):
562
+
563
+ def setUp(self):
564
+ cache.clear()
565
+
566
+ def test_sleep(self):
567
+ @wait_for_esi_errorlimit_reset()
568
+ def my_func():
569
+ return "Pass"
570
+
571
+ _t = time()
572
+ my_func()
573
+ self.assertLess(time() - _t, 1)
574
+ cache.set("esi_error_limit_reset", 5, timeout=5)
575
+ _t = time()
576
+ my_func()
577
+ duration = time() - _t
578
+ self.assertGreater(duration, 5)