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,116 @@
1
+ from datetime import timedelta
2
+ from unittest.mock import patch
3
+
4
+ from celery import current_app as celery_app
5
+ from django.utils.timezone import now
6
+
7
+ from esi.models import CallbackRedirect, Token
8
+ from esi.tasks import cleanup_callbackredirect, cleanup_token, cleanup_token_subset
9
+
10
+ from . import NoSocketsTestCase
11
+ from .factories_2 import TokenFactory, CallbackRedirectFactory
12
+
13
+ from math import ceil
14
+
15
+ MANAGERS_PATH = "esi.managers"
16
+ MODELS_PATH = "esi.models"
17
+ TASKS_PATH = "esi.tasks"
18
+
19
+
20
+ class CeleryTestCase(NoSocketsTestCase):
21
+ @classmethod
22
+ def setUpClass(cls):
23
+ super().setUpClass()
24
+ celery_app.conf.task_always_eager = True
25
+ celery_app.conf.task_eager_propagates = True
26
+
27
+
28
+ class TestCleanupCallbackredirect(CeleryTestCase):
29
+ def test_should_remove_expired(self) -> None:
30
+ # given
31
+ cb_valid = CallbackRedirectFactory()
32
+ with patch("django.utils.timezone.now") as m:
33
+ m.return_value = now() - timedelta(minutes=5, seconds=1)
34
+ cb_expired = CallbackRedirectFactory()
35
+ # when
36
+ cleanup_callbackredirect.delay(max_age=300)
37
+ # then
38
+ self.assertTrue(CallbackRedirect.objects.filter(pk=cb_valid.pk).exists())
39
+ self.assertFalse(CallbackRedirect.objects.filter(pk=cb_expired.pk).exists())
40
+
41
+
42
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 120)
43
+ @patch(MODELS_PATH + '.Token.refresh', spec=True)
44
+ class TestCleanupToken(CeleryTestCase):
45
+ def test_should_delete_orphaned_tokens(self, mock_token_refresh) -> None:
46
+ # given
47
+ token_1 = TokenFactory(user=None)
48
+ token_2 = TokenFactory()
49
+ # when
50
+ cleanup_token.delay()
51
+ # then
52
+ self.assertFalse(Token.objects.filter(pk=token_1.pk).exists())
53
+ self.assertTrue(Token.objects.filter(pk=token_2.pk).exists())
54
+
55
+ def test_should_refresh_expired_tokens_only(self, mock_token_refresh) -> None:
56
+ # given
57
+ TokenFactory()
58
+ with patch("django.utils.timezone.now") as m:
59
+ m.return_value = now() - timedelta(minutes=3)
60
+ TokenFactory()
61
+ # when
62
+ cleanup_token.delay()
63
+ # then
64
+ self.assertEqual(mock_token_refresh.call_count, 1)
65
+
66
+
67
+ class TestCleanupTokenSubset(CeleryTestCase):
68
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 120)
69
+ @patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
70
+ def test_should_delete_orphaned_tokens(self, mock_token_refresh) -> None:
71
+ # given
72
+ orphaned = TokenFactory(user=None)
73
+ valid = TokenFactory()
74
+ # when
75
+ cleanup_token_subset.delay()
76
+ # then
77
+ self.assertFalse(Token.objects.filter(pk=orphaned.pk).exists())
78
+ self.assertTrue(Token.objects.filter(pk=valid.pk).exists())
79
+
80
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
81
+ @patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
82
+ def test_should_refresh_fraction_of_expired_tokens(self, mock_token_refresh) -> None:
83
+ # given
84
+ num_expired = 100
85
+ for _ in range(num_expired):
86
+ TokenFactory(refresh_token="some_token")
87
+ # patch time so all tokens are expired
88
+ with patch("django.utils.timezone.now") as m:
89
+ m.return_value = now() + timedelta(minutes=5)
90
+ # when
91
+ cleanup_token_subset.delay(fraction=10)
92
+ # then
93
+ expected_count = ceil(num_expired / 10)
94
+ self.assertEqual(mock_token_refresh.call_count, expected_count)
95
+
96
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
97
+ @patch("esi.tasks.refresh_or_delete_token.apply_async")
98
+ def test_should_refresh_expired_tokens_only(self, mock_token_refresh) -> None:
99
+ # given
100
+ TokenFactory()
101
+ with patch("django.utils.timezone.now") as m:
102
+ m.return_value = now() - timedelta(minutes=3)
103
+ TokenFactory()
104
+ # when
105
+ cleanup_token_subset.delay()
106
+ # then
107
+ self.assertEqual(mock_token_refresh.call_count, 1)
108
+
109
+ @patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
110
+ @patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
111
+ def test_should_log_and_exit_gracefully_with_no_tokens(self, mock_token_refresh) -> None:
112
+ # when
113
+ cleanup_token_subset.delay()
114
+ # then
115
+ self.assertEqual(Token.objects.count(), 0)
116
+ mock_token_refresh.assert_not_called()
@@ -0,0 +1,22 @@
1
+ """unit tests for esi"""
2
+
3
+ from django.test import TestCase
4
+ from ..templatetags.scope_tags import scope_friendly_name
5
+
6
+
7
+ class TestScope(TestCase):
8
+ """tests for template tag"""
9
+
10
+ def test_friendly_name_fail(self):
11
+ x = scope_friendly_name('dummy_scope')
12
+ self.assertEqual(
13
+ "dummy_scope",
14
+ x
15
+ )
16
+
17
+ def test_friendly_name_pass(self):
18
+ x = scope_friendly_name('test.dummy_scope.test')
19
+ self.assertEqual(
20
+ "dummy scope",
21
+ x
22
+ )
@@ -0,0 +1,331 @@
1
+ from unittest.mock import patch
2
+
3
+ from django.contrib.auth.models import User
4
+ from django.contrib.sessions.middleware import SessionMiddleware
5
+ from django.http import (
6
+ HttpResponse,
7
+ HttpResponseRedirect,
8
+ Http404,
9
+ HttpResponseBadRequest
10
+ )
11
+ from django.test import TestCase, RequestFactory
12
+
13
+ from . import _generate_token, _store_as_Token
14
+ from ..models import CallbackRedirect
15
+ from ..views import sso_redirect, receive_callback, select_token
16
+
17
+
18
+ ESI_SSO_CLIENT_ID = 'abc'
19
+ ESI_SSO_CALLBACK_URL = 'https://www.example.com/callback/'
20
+ ESI_OAUTH_LOGIN_URL = 'https://www.example.com/oauth/'
21
+
22
+ redirect_sub_url = '/%s/' % ('x' * (2048 - 25))
23
+ redirect_url = 'https://www.example.com' + redirect_sub_url
24
+
25
+
26
+ class TestSsoCallbackView(TestCase):
27
+
28
+ def setUp(self):
29
+ self.user = User.objects.create_user(
30
+ 'Bruce Wayne',
31
+ 'abc@example.com',
32
+ 'password'
33
+ )
34
+ self.token = _store_as_Token(
35
+ _generate_token(
36
+ character_id=99,
37
+ character_name=self.user.username,
38
+ scopes=['abc', 'xyz', '123']
39
+ ),
40
+ self.user
41
+ )
42
+ self.factory = RequestFactory()
43
+ CallbackRedirect.objects.all().delete()
44
+
45
+ @patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
46
+ @patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
47
+ @patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
48
+ @patch('esi.views.OAuth2Session', autospec=True)
49
+ def test_redirect_to_url_no_scopes(self, mock_OAuth2Session):
50
+ state = 'my_awesome_state'
51
+ mock_OAuth2Session.return_value.authorization_url.return_value = \
52
+ (redirect_url, state)
53
+
54
+ request = self.factory.get(redirect_url)
55
+ request.user = self.user
56
+ middleware = SessionMiddleware(HttpResponse)
57
+ middleware.process_request(request)
58
+ request.session.save()
59
+
60
+ http_response = sso_redirect(request)
61
+
62
+ self.assertTrue(mock_OAuth2Session.called)
63
+ args, kwargs = mock_OAuth2Session.call_args
64
+ self.assertEqual(kwargs['scope'], [])
65
+
66
+ self.assertEqual(http_response.url, redirect_url)
67
+
68
+ callback_redirects = (
69
+ CallbackRedirect.objects.filter(session_key=request.session.session_key)
70
+ )
71
+ self.assertEqual(len(callback_redirects), 1)
72
+ callback_redirect = callback_redirects.first()
73
+ self.assertEqual(callback_redirect.url, redirect_sub_url)
74
+ self.assertEqual(callback_redirect.session_key, request.session.session_key)
75
+ self.assertEqual(callback_redirect.state, state)
76
+
77
+ @patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
78
+ @patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
79
+ @patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
80
+ @patch('esi.views.OAuth2Session', autospec=True)
81
+ def test_redirect_to_url_w_single_scope(self, mock_OAuth2Session):
82
+ state = 'my_awesome_state'
83
+ mock_OAuth2Session.return_value.authorization_url.return_value = \
84
+ (redirect_url, state)
85
+
86
+ request = self.factory.get(redirect_url)
87
+ request.user = self.user
88
+ middleware = SessionMiddleware(HttpResponse)
89
+ middleware.process_request(request)
90
+ request.session.save()
91
+
92
+ http_response = sso_redirect(request, scopes='abc')
93
+ self.assertTrue(mock_OAuth2Session.called)
94
+ args, kwargs = mock_OAuth2Session.call_args
95
+ self.assertEqual(kwargs['scope'], ['abc'])
96
+
97
+ self.assertEqual(http_response.url, redirect_url)
98
+
99
+ callback_redirects = (
100
+ CallbackRedirect.objects.filter(session_key=request.session.session_key)
101
+ )
102
+ self.assertEqual(len(callback_redirects), 1)
103
+ callback_redirect = callback_redirects.first()
104
+ self.assertEqual(callback_redirect.url, redirect_sub_url)
105
+ self.assertEqual(callback_redirect.session_key, request.session.session_key)
106
+ self.assertEqual(callback_redirect.state, state)
107
+
108
+ @patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
109
+ @patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
110
+ @patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
111
+ @patch('esi.views.OAuth2Session', autospec=True)
112
+ def test_redirect_to_url_w_multiple_scopes(self, mock_OAuth2Session):
113
+ state = 'my_awesome_state'
114
+ mock_OAuth2Session.return_value.authorization_url.return_value = \
115
+ (redirect_url, state)
116
+
117
+ request = self.factory.get(redirect_url)
118
+ request.user = self.user
119
+ middleware = SessionMiddleware(HttpResponse)
120
+ middleware.process_request(request)
121
+ request.session.save()
122
+
123
+ http_response = sso_redirect(request, scopes=['abc', 'def'])
124
+ self.assertTrue(mock_OAuth2Session.called)
125
+ args, kwargs = mock_OAuth2Session.call_args
126
+ self.assertEqual(kwargs['scope'], ['abc', 'def'])
127
+
128
+ self.assertEqual(http_response.url, redirect_url)
129
+
130
+ callback_redirects = CallbackRedirect.objects\
131
+ .filter(session_key=request.session.session_key)
132
+ self.assertEqual(len(callback_redirects), 1)
133
+ callback_redirect = callback_redirects.first()
134
+ self.assertEqual(callback_redirect.url, redirect_sub_url)
135
+ self.assertEqual(callback_redirect.session_key, request.session.session_key)
136
+ self.assertEqual(callback_redirect.state, state)
137
+
138
+ @patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
139
+ @patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
140
+ @patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
141
+ @patch('esi.views.reverse', autospec=True)
142
+ @patch('esi.views.OAuth2Session', autospec=True)
143
+ def test_redirect_to_view_no_scopes(self, mock_OAuth2Session, mock_reverse):
144
+ state = 'my_awesome_state'
145
+ mock_OAuth2Session.return_value.authorization_url.return_value = \
146
+ (redirect_url, state)
147
+ my_view_url = '/my_view/'
148
+ mock_reverse.return_value = my_view_url
149
+
150
+ request = self.factory.get('https://www.example.com/callback2/')
151
+ request.user = self.user
152
+ middleware = SessionMiddleware(HttpResponse)
153
+ middleware.process_request(request)
154
+ request.session.save()
155
+
156
+ http_response = sso_redirect(request, return_to='my_view')
157
+
158
+ self.assertTrue(mock_OAuth2Session.called)
159
+ args, kwargs = mock_OAuth2Session.call_args
160
+ self.assertEqual(kwargs['scope'], [])
161
+
162
+ self.assertEqual(http_response.url, redirect_url)
163
+
164
+ callback_redirects = (
165
+ CallbackRedirect.objects.filter(session_key=request.session.session_key)
166
+ )
167
+ self.assertEqual(len(callback_redirects), 1)
168
+ callback_redirect = callback_redirects.first()
169
+ self.assertEqual(callback_redirect.url, my_view_url)
170
+ self.assertEqual(callback_redirect.session_key, request.session.session_key)
171
+ self.assertEqual(callback_redirect.state, state)
172
+
173
+ @patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
174
+ @patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
175
+ @patch('esi.views.reverse')
176
+ def test_sso_redirect_return_to(self, mock_reverse):
177
+ mock_reverse.return_value = '/callback3/'
178
+
179
+ request = self.factory.get('https://www.example.com/callback2/')
180
+ request.user = self.user
181
+ middleware = SessionMiddleware(HttpResponse)
182
+ middleware.process_request(request)
183
+ request.session.save()
184
+
185
+ sso_redirect(request, return_to='callback3')
186
+
187
+ callback_redirects = (
188
+ CallbackRedirect.objects.filter(session_key=request.session.session_key)
189
+ )
190
+ self.assertEqual(len(callback_redirects), 1)
191
+ callback_redirect = callback_redirects.first()
192
+ self.assertEqual(callback_redirect.url, '/callback3/')
193
+ self.assertEqual(callback_redirect.session_key, request.session.session_key)
194
+
195
+ @patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
196
+ @patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
197
+ def test_sso_redirect_start_session(self):
198
+ request = self.factory.get('https://www.example.com/callback2/')
199
+ request.user = self.user
200
+ middleware = SessionMiddleware(HttpResponse)
201
+ middleware.process_request(request)
202
+
203
+ sso_redirect(request)
204
+
205
+ callback_redirects = (
206
+ CallbackRedirect.objects.filter(session_key=request.session.session_key)
207
+ )
208
+ self.assertEqual(len(callback_redirects), 1)
209
+ callback_redirect = callback_redirects.first()
210
+ self.assertEqual(callback_redirect.url, '/callback2/')
211
+ self.assertEqual(callback_redirect.session_key, request.session.session_key)
212
+
213
+
214
+ class TesReceiveCallbackView(TestCase):
215
+
216
+ def setUp(self):
217
+ self.user = User.objects.create_user(
218
+ 'Bruce Wayne',
219
+ 'abc@example.com',
220
+ 'password'
221
+ )
222
+
223
+ self.token = _store_as_Token(
224
+ _generate_token(
225
+ character_id=99,
226
+ character_name=self.user.username,
227
+ scopes=['abc', 'xyz', '123']
228
+ ),
229
+ self.user
230
+ )
231
+ self.factory = RequestFactory()
232
+ CallbackRedirect.objects.all().delete()
233
+
234
+ @patch('esi.managers.TokenManager.create_from_request')
235
+ def test_normal(self, mock_create_from_request):
236
+ mock_create_from_request.return_value = self.token
237
+
238
+ request = self.factory.get('https://www.example.com?code=abc&state=123')
239
+ request.user = self.user
240
+ middleware = SessionMiddleware(HttpResponse)
241
+ middleware.process_request(request)
242
+ request.session.save()
243
+
244
+ callback_redirect = CallbackRedirect.objects.create(
245
+ session_key=request.session.session_key,
246
+ state='123',
247
+ url=redirect_url
248
+ )
249
+ http_response = receive_callback(request)
250
+ callback_redirect.refresh_from_db()
251
+ self.assertIsInstance(http_response, HttpResponseRedirect)
252
+ self.assertEqual(http_response.url, redirect_url)
253
+ self.assertEqual(callback_redirect.token, self.token)
254
+
255
+ def test_missing_code(self):
256
+ request = self.factory.get('https://www.example.com?state=123')
257
+ request.user = self.user
258
+ middleware = SessionMiddleware(HttpResponse)
259
+ middleware.process_request(request)
260
+ request.session.save()
261
+
262
+ http_response = receive_callback(request)
263
+ self.assertIsInstance(http_response, HttpResponseBadRequest)
264
+
265
+ def test_missing_state(self):
266
+ request = self.factory.get('https://www.example.com?code=abc')
267
+ request.user = self.user
268
+ middleware = SessionMiddleware(HttpResponse)
269
+ middleware.process_request(request)
270
+ request.session.save()
271
+
272
+ http_response = receive_callback(request)
273
+ self.assertIsInstance(http_response, HttpResponseBadRequest)
274
+
275
+ def test_missing_callback(self):
276
+ request = self.factory.get('https://www.example.com?code=abc&state=123')
277
+ request.user = self.user
278
+ middleware = SessionMiddleware(HttpResponse)
279
+ middleware.process_request(request)
280
+ request.session.save()
281
+
282
+ with self.assertRaises(Http404):
283
+ receive_callback(request)
284
+
285
+
286
+ class TestSelectTokenView(TestCase):
287
+ def setUp(self):
288
+ self.user = User.objects.create_user(
289
+ 'Bruce Wayne',
290
+ 'abc@example.com',
291
+ 'password'
292
+ )
293
+ self.token = _store_as_Token(
294
+ _generate_token(
295
+ character_id=99,
296
+ character_name=self.user.username,
297
+ scopes=['abc', 'xyz', '123']
298
+ ),
299
+ self.user
300
+ )
301
+ self.factory = RequestFactory()
302
+ CallbackRedirect.objects.all().delete()
303
+
304
+ def test_should_render_select_page_on_first_call(self):
305
+ # given
306
+ request = self.factory.get('https://www.example.com/my_view/')
307
+ request.user = self.user
308
+ middleware = SessionMiddleware(HttpResponse)
309
+ middleware.process_request(request)
310
+ request.session.save()
311
+ # when
312
+ response = select_token(request, scopes='abc')
313
+ # then
314
+ self.assertIn("ESI Token Selection", response.content.decode("utf-8"))
315
+
316
+ def test_should_render_select_page_on_second_call(self):
317
+ # given
318
+ request = self.factory.get('https://www.example.com/my_view/')
319
+ request.user = self.user
320
+ middleware = SessionMiddleware(HttpResponse)
321
+ middleware.process_request(request)
322
+ request.session.save()
323
+ CallbackRedirect.objects.create(
324
+ session_key=request.session.session_key,
325
+ state='state123',
326
+ token=self.token
327
+ )
328
+ # when
329
+ response = select_token(request, scopes='abc', new=True)
330
+ # then
331
+ self.assertIn("ESI Token Selection", response.content.decode("utf-8"))
@@ -0,0 +1,69 @@
1
+ # flake8: noqa
2
+ """script for testing connection pool size with live requests to ESI
3
+
4
+ Run this script directly. Make sure to also set the environment variable
5
+ DJANGO_PROJECT_PATH and DJANGO_SETTINGS_MODULE to match your setup:
6
+
7
+ You can see the result in your main log file of your Django installation.
8
+ If your connection pool size is too small you will see the following warning in your
9
+ log file: "Connection pool is full, discarding connection: esi.evetech.net"
10
+ To pass this test successfully ESI_CONNECTION_POOL_MAXSIZE must equal to number
11
+ of MAX_WORKER (e.g. 20)
12
+
13
+ Example:
14
+ export DJANGO_PROJECT_PATH="/home/erik997/dev/python/django-dev/mysite"
15
+ export DJANGO_SETTINGS_MODULE="mysite.settings"
16
+
17
+ """
18
+
19
+ # start django project
20
+ import os
21
+ import sys
22
+
23
+ if not 'DJANGO_PROJECT_PATH' in os.environ:
24
+ print('DJANGO_PROJECT_PATH is not set')
25
+ exit(1)
26
+
27
+ if not 'DJANGO_SETTINGS_MODULE' in os.environ:
28
+ print('DJANGO_SETTINGS_MODULE is not set')
29
+ exit(1)
30
+
31
+ sys.path.insert(0, os.environ['DJANGO_PROJECT_PATH'])
32
+ import django
33
+ django.setup()
34
+
35
+ # normal imports
36
+ import concurrent.futures
37
+ import logging
38
+
39
+ from django.core.cache import cache
40
+ from esi.clients import EsiClientProvider
41
+
42
+ MAX_WORKER = 20
43
+
44
+ cache.clear()
45
+ esi = EsiClientProvider()
46
+
47
+ logger = logging.getLogger('__name__')
48
+ logger.level = logging.DEBUG
49
+ logger.propagate = True
50
+
51
+
52
+ def thread_func(type_id):
53
+ logger.info('Fetching type with ID %d from ESI', type_id)
54
+ esi.client.Universe.get_universe_types_type_id(type_id=type_id).result()
55
+
56
+ def main():
57
+ print('Script started...')
58
+ entity_ids = esi.client.Universe.get_universe_types().results()[1000:2000]
59
+ logger.info('Start fetching %d types' , len(entity_ids))
60
+
61
+ with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKER) as executor:
62
+ executor.map(thread_func, entity_ids)
63
+
64
+ logger.info('Finished fetching %d types', len(entity_ids))
65
+ print('DONE')
66
+
67
+
68
+ if __name__ == '__main__':
69
+ main()
esi/urls.py ADDED
@@ -0,0 +1,9 @@
1
+ from django.urls import path
2
+
3
+ from . import views
4
+
5
+ app_name = 'esi'
6
+
7
+ urlpatterns = [
8
+ path('callback/', views.receive_callback, name='callback'),
9
+ ]
esi/views.py ADDED
@@ -0,0 +1,129 @@
1
+ import logging
2
+
3
+ from requests_oauthlib import OAuth2Session
4
+
5
+ from django.http.response import HttpResponseBadRequest
6
+ from django.shortcuts import get_object_or_404, redirect, render
7
+ from django.urls import reverse
8
+
9
+ from esi import app_settings
10
+ from esi.models import CallbackRedirect, Token
11
+
12
+ from .decorators import tokens_required
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def sso_redirect(request, scopes=None, return_to=None):
18
+ """
19
+ Generates a :model:`esi.CallbackRedirect` for the specified request.
20
+ Redirects to EVE for login.
21
+ Accepts a view or URL name as a redirect after SSO.
22
+ """
23
+ logger.debug(
24
+ "Initiating redirect of %s session %s",
25
+ request.user,
26
+ request.session.session_key[:5] if request.session.session_key else '[no key]'
27
+ )
28
+ if scopes is None:
29
+ scopes = list()
30
+ elif isinstance(scopes, str):
31
+ scopes = list([scopes])
32
+
33
+ # ensure only one callback redirect model per session
34
+ if request.session.session_key:
35
+ CallbackRedirect.objects\
36
+ .filter(session_key=request.session.session_key).delete()
37
+
38
+ # ensure we have a session
39
+ if not request.session.exists(request.session.session_key):
40
+ logger.debug("Creating new session before redirect.")
41
+ request.session.create()
42
+
43
+ if return_to:
44
+ url = reverse(return_to)
45
+ else:
46
+ url = request.get_full_path()
47
+
48
+ oauth = OAuth2Session(
49
+ app_settings.ESI_SSO_CLIENT_ID,
50
+ redirect_uri=app_settings.ESI_SSO_CALLBACK_URL,
51
+ scope=scopes
52
+ )
53
+ redirect_url, state = oauth.authorization_url(app_settings.ESI_OAUTH_LOGIN_URL)
54
+
55
+ CallbackRedirect.objects.create(
56
+ session_key=request.session.session_key, state=state, url=url
57
+ )
58
+ logger.debug(
59
+ "Redirecting %s session %s to SSO. Callback will be redirected to %s",
60
+ request.user,
61
+ request.session.session_key[:5],
62
+ url
63
+ )
64
+ return redirect(redirect_url)
65
+
66
+
67
+ def receive_callback(request):
68
+ """
69
+ Parses SSO callback, validates, retrieves :model:`esi.Token`,
70
+ and internally redirects to the target url.
71
+ """
72
+ logger.debug(
73
+ "Received callback for %s session %s",
74
+ request.user,
75
+ request.session.session_key[:5]
76
+ )
77
+ # make sure request has required parameters
78
+ code = request.GET.get('code', None)
79
+ state = request.GET.get('state', None)
80
+ if not code or not state:
81
+ logger.warning("Missing parameters for code exchange.")
82
+ return HttpResponseBadRequest()
83
+
84
+ callback = get_object_or_404(
85
+ CallbackRedirect, state=state, session_key=request.session.session_key
86
+ )
87
+ token = Token.objects.create_from_request(request)
88
+ callback.token = token
89
+ callback.save()
90
+ logger.debug(
91
+ "Processed callback for %s session %s. Redirecting to %s",
92
+ request.user,
93
+ request.session.session_key[:5],
94
+ callback.url
95
+ )
96
+ return redirect(callback.url)
97
+
98
+
99
+ def select_token(request, scopes='', new=False):
100
+ """
101
+ Presents the user with a selection of applicable tokens for the requested view.
102
+ """
103
+
104
+ @tokens_required(scopes=scopes, new=new)
105
+ def _token_list(r, tokens):
106
+ # Single Scope as string will break the Parser in the template
107
+ if isinstance(scopes, str):
108
+ scopes_output = [scopes]
109
+ else:
110
+ scopes_output = scopes
111
+
112
+ # fake distint on character_name to not show duplicates
113
+ # MySQL doesn't support distint on field.
114
+ token_output = []
115
+ _characters = set()
116
+ for t in tokens:
117
+ if t.character_name in _characters:
118
+ continue
119
+ token_output.append(t)
120
+ _characters.add(t.character_name)
121
+
122
+ context = {
123
+ 'tokens': token_output,
124
+ 'scopes': scopes_output
125
+ }
126
+
127
+ return render(r, 'esi/select_token.html', context=context)
128
+
129
+ return _token_list(request)