python-engineio 4.14.0__tar.gz → 4.15.0__tar.gz

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 (66) hide show
  1. {python_engineio-4.14.0/src/python_engineio.egg-info → python_engineio-4.15.0}/PKG-INFO +8 -7
  2. {python_engineio-4.14.0 → python_engineio-4.15.0}/pyproject.toml +8 -7
  3. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_client.py +18 -6
  4. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_server.py +26 -13
  5. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/base_client.py +8 -2
  6. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/client.py +8 -1
  7. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/server.py +26 -13
  8. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/static_files.py +2 -0
  9. {python_engineio-4.14.0 → python_engineio-4.15.0/src/python_engineio.egg-info}/PKG-INFO +8 -7
  10. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/python_engineio.egg-info/SOURCES.txt +1 -0
  11. python_engineio-4.15.0/tests/async/.test_client.py.swp +0 -0
  12. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/test_asgi.py +2 -0
  13. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/test_client.py +155 -71
  14. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/test_server.py +12 -0
  15. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/test_client.py +37 -10
  16. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/test_middleware.py +2 -0
  17. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/test_server.py +11 -0
  18. {python_engineio-4.14.0 → python_engineio-4.15.0}/tox.ini +19 -0
  19. {python_engineio-4.14.0 → python_engineio-4.15.0}/LICENSE +0 -0
  20. {python_engineio-4.14.0 → python_engineio-4.15.0}/MANIFEST.in +0 -0
  21. {python_engineio-4.14.0 → python_engineio-4.15.0}/README.md +0 -0
  22. {python_engineio-4.14.0 → python_engineio-4.15.0}/docs/_static/README.md +0 -0
  23. {python_engineio-4.14.0 → python_engineio-4.15.0}/setup.cfg +0 -0
  24. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/__init__.py +0 -0
  25. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/__init__.py +0 -0
  26. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/_websocket_wsgi.py +0 -0
  27. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/aiohttp.py +0 -0
  28. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/asgi.py +0 -0
  29. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/eventlet.py +0 -0
  30. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/gevent.py +0 -0
  31. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/gevent_uwsgi.py +0 -0
  32. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/sanic.py +0 -0
  33. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/threading.py +0 -0
  34. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_drivers/tornado.py +0 -0
  35. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/async_socket.py +0 -0
  36. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/base_server.py +0 -0
  37. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/base_socket.py +0 -0
  38. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/exceptions.py +0 -0
  39. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/json.py +0 -0
  40. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/middleware.py +0 -0
  41. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/packet.py +0 -0
  42. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/payload.py +0 -0
  43. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/engineio/socket.py +0 -0
  44. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/python_engineio.egg-info/dependency_links.txt +0 -0
  45. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/python_engineio.egg-info/not-zip-safe +0 -0
  46. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/python_engineio.egg-info/requires.txt +0 -0
  47. {python_engineio-4.14.0 → python_engineio-4.15.0}/src/python_engineio.egg-info/top_level.txt +0 -0
  48. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/__init__.py +0 -0
  49. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/index.html +0 -0
  50. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/test_aiohttp.py +0 -0
  51. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/test_sanic.py +0 -0
  52. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/test_socket.py +0 -0
  53. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/async/test_tornado.py +0 -0
  54. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/__init__.py +0 -0
  55. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/index.html +0 -0
  56. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/test_packet.py +0 -0
  57. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/test_payload.py +0 -0
  58. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/common/test_socket.py +0 -0
  59. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/README.md +0 -0
  60. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/binary_b64_packet.py +0 -0
  61. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/binary_packet.py +0 -0
  62. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/json_packet.py +0 -0
  63. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/payload.py +0 -0
  64. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/run.sh +0 -0
  65. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/server_receive.py +0 -0
  66. {python_engineio-4.14.0 → python_engineio-4.15.0}/tests/performance/text_packet.py +0 -0
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-engineio
3
- Version: 4.14.0
3
+ Version: 4.15.0
4
4
  Summary: Engine.IO server and client for Python
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
- License: MIT
7
- Project-URL: homepage, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
8
- Project-URL: documentation, https://python-engineio.readthedocs.io/
9
- Project-URL: repository, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
10
- Project-URL: issues, https://github.com/miguelgrinberg/python-engineio/issues
11
- Project-URL: changelog, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/src/branch/main/CHANGES.md
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
8
+ Project-URL: Documentation, https://python-engineio.readthedocs.io/
9
+ Project-URL: Source Code, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
10
+ Project-URL: Issue Tracker, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/issues
11
+ Project-URL: Changelog, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/src/branch/main/CHANGES.md
12
+ Project-URL: Funding, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio#sponsor-this-project
12
13
  Classifier: Environment :: Web Environment
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: Programming Language :: Python :: 3
@@ -1,7 +1,6 @@
1
1
  [project]
2
2
  name = "python-engineio"
3
- version = "4.14.0"
4
- license = {text = "MIT"}
3
+ version = "4.15.0"
5
4
  authors = [{name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com"}]
6
5
  description = "Engine.IO server and client for Python"
7
6
  classifiers = [
@@ -10,6 +9,7 @@ classifiers = [
10
9
  "Programming Language :: Python :: 3",
11
10
  "Operating System :: OS Independent",
12
11
  ]
12
+ license = "MIT"
13
13
  requires-python = ">=3.8"
14
14
  dependencies = ["simple-websocket >= 0.10.0"]
15
15
 
@@ -18,11 +18,12 @@ file = "README.md"
18
18
  content-type = "text/markdown"
19
19
 
20
20
  [project.urls]
21
- homepage = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio"
22
- documentation = "https://python-engineio.readthedocs.io/"
23
- repository = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio"
24
- issues = "https://github.com/miguelgrinberg/python-engineio/issues"
25
- changelog = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/src/branch/main/CHANGES.md"
21
+ Homepage = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio"
22
+ Documentation = "https://python-engineio.readthedocs.io/"
23
+ "Source Code" = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio"
24
+ "Issue Tracker" = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/issues"
25
+ Changelog = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/src/branch/main/CHANGES.md"
26
+ Funding = "https://code.miguelgrinberg.com/miguelgrinberg/python-engineio#sponsor-this-project"
26
27
 
27
28
  [project.optional-dependencies]
28
29
  client = [
@@ -226,6 +226,7 @@ class AsyncClient(base_client.BaseClient):
226
226
  if not self.external_http: # pragma: no cover
227
227
  if self.http and not self.http.closed:
228
228
  await self.http.close()
229
+ self.http = None
229
230
 
230
231
  def __del__(self): # pragma: no cover
231
232
  # try to close the aiohttp session if it is still open
@@ -323,8 +324,13 @@ class AsyncClient(base_client.BaseClient):
323
324
  self.logger.info(
324
325
  'Attempting WebSocket connection to ' + websocket_url)
325
326
 
326
- if self.http is None or self.http.closed: # pragma: no cover
327
- self.http = aiohttp.ClientSession()
327
+ if self.http is None or self.http.closed:
328
+ if self.http_factory:
329
+ self.http = self.http_factory()
330
+ if inspect.iscoroutine(self.http):
331
+ self.http = await self.http
332
+ else:
333
+ self.http = aiohttp.ClientSession()
328
334
 
329
335
  # extract any new cookies passed in a header so that they can also be
330
336
  # sent the the WebSocket route
@@ -458,12 +464,17 @@ class AsyncClient(base_client.BaseClient):
458
464
 
459
465
  async def _send_request(
460
466
  self, method, url, headers=None, body=None,
461
- timeout=None): # pragma: no cover
467
+ timeout=None):
462
468
  if self.http is None or self.http.closed:
463
- self.http = aiohttp.ClientSession()
469
+ if self.http_factory:
470
+ self.http = self.http_factory()
471
+ if inspect.iscoroutine(self.http):
472
+ self.http = await self.http
473
+ else:
474
+ self.http = aiohttp.ClientSession()
464
475
  http_method = getattr(self.http, method.lower())
465
476
 
466
- try:
477
+ try: # pragma: no cover
467
478
  if not self.ssl_verify:
468
479
  return await http_method(
469
480
  url, headers=headers, data=body,
@@ -473,7 +484,8 @@ class AsyncClient(base_client.BaseClient):
473
484
  url, headers=headers, data=body,
474
485
  timeout=aiohttp.ClientTimeout(total=timeout))
475
486
 
476
- except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
487
+ except (aiohttp.ClientError, asyncio.TimeoutError) \
488
+ as exc: # pragma: no cover
477
489
  self.logger.info('HTTP %s request to %s failed with error %s.',
478
490
  method, url, exc)
479
491
  return str(exc)
@@ -298,7 +298,13 @@ class AsyncServer(base_server.BaseServer):
298
298
  self._log_error_once(f'{e} {sid}', 'bad-sid')
299
299
  r = self._bad_request(f'{e} {sid}')
300
300
  else:
301
- if self.transport(sid) != transport and \
301
+ if socket.upgraded:
302
+ # cannot accept HTTP requests once upgraded
303
+ self._log_error_once(
304
+ f'Unexpected HTTP request for session {sid}',
305
+ 'unexpected-http')
306
+ r = self._bad_request('Unexpected HTTP request')
307
+ elif transport != 'polling' and \
302
308
  transport != upgrade_header:
303
309
  self._log_error_once(
304
310
  f'Invalid transport for session {sid}',
@@ -326,18 +332,25 @@ class AsyncServer(base_server.BaseServer):
326
332
  r = self._bad_request(f'Invalid session {sid}')
327
333
  else:
328
334
  socket = self._get_socket(sid)
329
- try:
330
- await socket.handle_post_request(environ)
331
- r = self._ok(jsonp_index=jsonp_index)
332
- except exceptions.EngineIOError:
333
- if sid in self.sockets: # pragma: no cover
334
- await self.disconnect(sid)
335
- r = self._bad_request()
336
- except: # pragma: no cover
337
- # for any other unexpected errors, we log the error
338
- # and keep going
339
- self.logger.exception('post request handler error')
340
- r = self._ok(jsonp_index=jsonp_index)
335
+ if not socket.upgraded:
336
+ try:
337
+ await socket.handle_post_request(environ)
338
+ r = self._ok(jsonp_index=jsonp_index)
339
+ except exceptions.EngineIOError:
340
+ if sid in self.sockets: # pragma: no cover
341
+ await self.disconnect(sid)
342
+ r = self._bad_request()
343
+ except: # pragma: no cover
344
+ # for any other unexpected errors, we log the error
345
+ # and keep going
346
+ self.logger.exception('post request handler error')
347
+ r = self._ok(jsonp_index=jsonp_index)
348
+ else:
349
+ # cannot accept HTTP requests once upgraded
350
+ self._log_error_once(
351
+ f'Unexpected HTTP request for session {sid}',
352
+ 'unexpected-http')
353
+ r = self._bad_request('Unexpected HTTP request')
341
354
  elif method == 'OPTIONS':
342
355
  r = self._ok()
343
356
  else:
@@ -55,8 +55,14 @@ class BaseClient:
55
55
  self.upgrades = None
56
56
  self.ping_interval = None
57
57
  self.ping_timeout = None
58
- self.http = http_session
59
- self.external_http = http_session is not None
58
+ self.http = None
59
+ self.http_factory = None
60
+ if http_session is not None:
61
+ if callable(http_session):
62
+ self.http_factory = http_session
63
+ else:
64
+ self.http = http_session
65
+ self.external_http = self.http is not None
60
66
  self.handle_sigint = handle_sigint
61
67
  self.ws = None
62
68
  self.read_loop_task = None
@@ -183,6 +183,8 @@ class Client(base_client.BaseClient):
183
183
  self.queue.task_done()
184
184
  except self.queue_empty:
185
185
  break
186
+ if not self.external_http: # pragma: no cover
187
+ self.http = None
186
188
 
187
189
  def _connect_polling(self, url, headers, engineio_path):
188
190
  """Establish a long-polling connection to the Engine.IO server."""
@@ -274,6 +276,8 @@ class Client(base_client.BaseClient):
274
276
  # so that they are preserved when connecting to the WebSocket route
275
277
  cookies = None
276
278
  extra_options = {}
279
+ if self.http is None and self.http_factory:
280
+ self.http = self.http_factory()
277
281
  if self.http:
278
282
  # cookies
279
283
  ck = SimpleCookie()
@@ -458,7 +462,10 @@ class Client(base_client.BaseClient):
458
462
  self, method, url, headers=None, body=None,
459
463
  timeout=None): # pragma: no cover
460
464
  if self.http is None:
461
- self.http = requests.Session()
465
+ if self.http_factory:
466
+ self.http = self.http_factory()
467
+ else:
468
+ self.http = requests.Session()
462
469
  if not self.ssl_verify:
463
470
  self.http.verify = False
464
471
  try:
@@ -276,7 +276,13 @@ class Server(base_server.BaseServer):
276
276
  self._log_error_once(f'{e} {sid}', 'bad-sid')
277
277
  r = self._bad_request(f'{e} {sid}')
278
278
  else:
279
- if self.transport(sid) != transport and \
279
+ if socket.upgraded:
280
+ # cannot accept HTTP requests once upgraded
281
+ self._log_error_once(
282
+ f'Unexpected HTTP request for session {sid}',
283
+ 'unexpected-http')
284
+ r = self._bad_request('Unexpected HTTP request')
285
+ elif transport != 'polling' and \
280
286
  transport != upgrade_header:
281
287
  self._log_error_once(
282
288
  f'Invalid transport for session {sid}',
@@ -304,18 +310,25 @@ class Server(base_server.BaseServer):
304
310
  r = self._bad_request(f'Invalid session {sid}')
305
311
  else:
306
312
  socket = self._get_socket(sid)
307
- try:
308
- socket.handle_post_request(environ)
309
- r = self._ok(jsonp_index=jsonp_index)
310
- except exceptions.EngineIOError:
311
- if sid in self.sockets: # pragma: no cover
312
- self.disconnect(sid)
313
- r = self._bad_request()
314
- except: # pragma: no cover
315
- # for any other unexpected errors, we log the error
316
- # and keep going
317
- self.logger.exception('post request handler error')
318
- r = self._ok(jsonp_index=jsonp_index)
313
+ if not socket.upgraded:
314
+ try:
315
+ socket.handle_post_request(environ)
316
+ r = self._ok(jsonp_index=jsonp_index)
317
+ except exceptions.EngineIOError:
318
+ if sid in self.sockets: # pragma: no cover
319
+ self.disconnect(sid)
320
+ r = self._bad_request()
321
+ except: # pragma: no cover
322
+ # for any other unexpected errors, we log the error
323
+ # and keep going
324
+ self.logger.exception('post request handler error')
325
+ r = self._ok(jsonp_index=jsonp_index)
326
+ else:
327
+ # cannot accept HTTP requests once upgraded
328
+ self._log_error_once(
329
+ f'Unexpected HTTP request for session {sid}',
330
+ 'unexpected-http')
331
+ r = self._bad_request('Unexpected HTTP request')
319
332
  elif method == 'OPTIONS':
320
333
  r = self._ok()
321
334
  else:
@@ -35,6 +35,8 @@ def get_static_file(path, static_files):
35
35
  elif path + '/' in static_files:
36
36
  f = static_files[path + '/']
37
37
  break
38
+ if '../' in extra_path:
39
+ return None
38
40
  if f:
39
41
  if isinstance(f, str):
40
42
  f = {'filename': f}
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-engineio
3
- Version: 4.14.0
3
+ Version: 4.15.0
4
4
  Summary: Engine.IO server and client for Python
5
5
  Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
6
- License: MIT
7
- Project-URL: homepage, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
8
- Project-URL: documentation, https://python-engineio.readthedocs.io/
9
- Project-URL: repository, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
10
- Project-URL: issues, https://github.com/miguelgrinberg/python-engineio/issues
11
- Project-URL: changelog, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/src/branch/main/CHANGES.md
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
8
+ Project-URL: Documentation, https://python-engineio.readthedocs.io/
9
+ Project-URL: Source Code, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio
10
+ Project-URL: Issue Tracker, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/issues
11
+ Project-URL: Changelog, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio/src/branch/main/CHANGES.md
12
+ Project-URL: Funding, https://code.miguelgrinberg.com/miguelgrinberg/python-engineio#sponsor-this-project
12
13
  Classifier: Environment :: Web Environment
13
14
  Classifier: Intended Audience :: Developers
14
15
  Classifier: Programming Language :: Python :: 3
@@ -36,6 +36,7 @@ src/python_engineio.egg-info/dependency_links.txt
36
36
  src/python_engineio.egg-info/not-zip-safe
37
37
  src/python_engineio.egg-info/requires.txt
38
38
  src/python_engineio.egg-info/top_level.txt
39
+ tests/async/.test_client.py.swp
39
40
  tests/async/__init__.py
40
41
  tests/async/index.html
41
42
  tests/async/test_aiohttp.py
@@ -147,6 +147,8 @@ class TestAsgi:
147
147
  'Not Found')
148
148
  await check_path('/bar/foo', 404, 'text/plain', 'Not Found')
149
149
  await check_path('', 404, 'text/plain', 'Not Found')
150
+ await check_path('/static/../../LICENSE', 404, 'text/plain',
151
+ 'Not Found')
150
152
 
151
153
  app.static_files[''] = 'index.html'
152
154
  await check_path('/static/test/', 200, 'text/html', '<html></html>\n')
@@ -133,16 +133,60 @@ class TestAsyncClient:
133
133
  match='Connect handler failed: Failed on purpose'):
134
134
  await c.connect('http://foo')
135
135
 
136
- @mock.patch('engineio.client.time.time', return_value=123.456)
137
- @mock.patch('engineio.async_client.aiohttp.ClientWSTimeout',
138
- new=mock_ws_timeout)
139
- async def test_connect_websocket_handler_failure(self, _time):
136
+ @mock.patch('engineio.async_client.aiohttp.ClientSession')
137
+ async def test_connect_websocket_default_session(self, _session):
138
+ _session().ws_connect = mock.AsyncMock()
139
+ ws = _session().ws_connect.return_value
140
+ ws.receive = mock.AsyncMock()
141
+ ws.receive.return_value.data = packet.Packet(
142
+ packet.MESSAGE, # invaid on purpose
143
+ {},
144
+ ).encode()
140
145
  c = async_client.AsyncClient()
141
- c.http = mock.MagicMock(closed=False)
146
+ with pytest.raises(exceptions.ConnectionError,
147
+ match='no OPEN packet'):
148
+ await c.connect('http://foo', transports='websocket')
149
+
150
+ async def test_connect_websocket_external_session(self):
151
+ c = async_client.AsyncClient()
152
+ c.http = mock.Mock(closed=False)
153
+ c.external_http = True
142
154
  c.http.ws_connect = mock.AsyncMock()
143
- c.http.close = mock.AsyncMock()
144
155
  ws = c.http.ws_connect.return_value
145
156
  ws.receive = mock.AsyncMock()
157
+ ws.receive.return_value.data = packet.Packet(
158
+ packet.MESSAGE, # invaid on purpose
159
+ {},
160
+ ).encode()
161
+ with pytest.raises(exceptions.ConnectionError,
162
+ match='no OPEN packet'):
163
+ await c.connect('http://foo', transports='websocket')
164
+
165
+ async def test_connect_websocket_async_session_factory(self):
166
+ async def get_session():
167
+ http = mock.MagicMock(closed=False)
168
+ http.ws_connect = mock.AsyncMock()
169
+ http.close = mock.AsyncMock()
170
+ ws = http.ws_connect.return_value
171
+ ws.receive = mock.AsyncMock()
172
+ ws.receive.return_value.data = packet.Packet(
173
+ packet.MESSAGE, # invaid on purpose
174
+ {},
175
+ ).encode()
176
+ return http
177
+
178
+ c = async_client.AsyncClient(http_session=get_session)
179
+ with pytest.raises(exceptions.ConnectionError,
180
+ match='no OPEN packet'):
181
+ await c.connect('http://foo', transports='websocket')
182
+
183
+ @mock.patch('engineio.client.time.time', return_value=123.456)
184
+ async def test_connect_websocket_handler_failure(self, _time):
185
+ http = mock.MagicMock(closed=False)
186
+ http.ws_connect = mock.AsyncMock()
187
+ http.close = mock.AsyncMock()
188
+ ws = http.ws_connect.return_value
189
+ ws.receive = mock.AsyncMock()
146
190
  ws.receive.return_value.data = packet.Packet(
147
191
  packet.OPEN,
148
192
  {
@@ -152,6 +196,7 @@ class TestAsyncClient:
152
196
  'pingTimeout': 2000,
153
197
  },
154
198
  ).encode()
199
+ c = async_client.AsyncClient(http_session=lambda: http)
155
200
 
156
201
  @c.on('connect')
157
202
  def connect():
@@ -161,6 +206,41 @@ class TestAsyncClient:
161
206
  match='Connect handler failed: Failed on purpose'):
162
207
  await c.connect('http://foo', transports='websocket')
163
208
 
209
+ @mock.patch('engineio.async_client.aiohttp.ClientSession')
210
+ async def test_send_request_with_default_session(self, _session):
211
+ _session().get = mock.AsyncMock()
212
+ c = async_client.AsyncClient()
213
+ await c._send_request('GET', '/socket.io')
214
+ _session().get.assert_awaited_once()
215
+
216
+ async def test_send_request_with_session_factory(self):
217
+ http = mock.Mock()
218
+ http.closed = False
219
+ http.get = mock.AsyncMock()
220
+ c = async_client.AsyncClient(http_session=lambda: http)
221
+ await c._send_request('GET', '/socket.io')
222
+ http.get.assert_awaited_once()
223
+
224
+ async def test_send_request_with_async_session_factory(self):
225
+ async def get_session():
226
+ http = mock.Mock()
227
+ http.closed = False
228
+ http.get = mock.AsyncMock()
229
+ return http
230
+
231
+ c = async_client.AsyncClient(http_session=get_session)
232
+ await c._send_request('GET', '/socket.io')
233
+ c.http.get.assert_awaited_once()
234
+
235
+ async def test_send_request_with_external_session(self):
236
+ c = async_client.AsyncClient()
237
+ c.http = mock.Mock()
238
+ c.http.closed = False
239
+ c.http.get = mock.AsyncMock()
240
+ c.external_http = True
241
+ await c._send_request('GET', '/socket.io')
242
+ c.http.get.assert_awaited_once()
243
+
164
244
  async def test_wait(self):
165
245
  c = async_client.AsyncClient()
166
246
  done = []
@@ -563,18 +643,18 @@ class TestAsyncClient:
563
643
  @mock.patch('engineio.async_client.aiohttp.ClientWSTimeout',
564
644
  new=mock_ws_timeout)
565
645
  async def test_websocket_connection_failed(self, _time):
566
- c = async_client.AsyncClient()
567
- c.http = mock.MagicMock(closed=False)
568
- c.http.ws_connect = mock.AsyncMock(
646
+ http = mock.MagicMock(closed=False)
647
+ http.ws_connect = mock.AsyncMock(
569
648
  side_effect=[aiohttp.client_exceptions.ServerConnectionError()]
570
649
  )
650
+ c = async_client.AsyncClient(http_session=lambda: http)
571
651
  with pytest.raises(exceptions.ConnectionError):
572
652
  await c.connect(
573
653
  'http://foo',
574
654
  transports=['websocket'],
575
655
  headers={'Foo': 'Bar'},
576
656
  )
577
- c.http.ws_connect.assert_awaited_once_with(
657
+ http.ws_connect.assert_awaited_once_with(
578
658
  'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456',
579
659
  headers={'Foo': 'Bar'},
580
660
  timeout=5
@@ -582,21 +662,24 @@ class TestAsyncClient:
582
662
 
583
663
  @mock.patch('engineio.client.time.time', return_value=123.456)
584
664
  async def test_websocket_connection_extra(self, _time):
585
- c = async_client.AsyncClient(websocket_extra_options={
586
- 'headers': {'Baz': 'Qux'},
587
- 'timeout': 10
588
- })
589
- c.http = mock.MagicMock(closed=False)
590
- c.http.ws_connect = mock.AsyncMock(
665
+ http = mock.MagicMock(closed=False)
666
+ http.ws_connect = mock.AsyncMock(
591
667
  side_effect=[aiohttp.client_exceptions.ServerConnectionError()]
592
668
  )
669
+ c = async_client.AsyncClient(
670
+ http_session=lambda: http,
671
+ websocket_extra_options={
672
+ 'headers': {'Baz': 'Qux'},
673
+ 'timeout': 10
674
+ }
675
+ )
593
676
  with pytest.raises(exceptions.ConnectionError):
594
677
  await c.connect(
595
678
  'http://foo',
596
679
  transports=['websocket'],
597
680
  headers={'Foo': 'Bar'},
598
681
  )
599
- c.http.ws_connect.assert_awaited_once_with(
682
+ http.ws_connect.assert_awaited_once_with(
600
683
  'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456',
601
684
  headers={'Foo': 'Bar', 'Baz': 'Qux'},
602
685
  timeout=10,
@@ -606,28 +689,28 @@ class TestAsyncClient:
606
689
  @mock.patch('engineio.async_client.aiohttp.ClientWSTimeout',
607
690
  new=mock_ws_timeout)
608
691
  async def test_websocket_upgrade_failed(self, _time):
609
- c = async_client.AsyncClient()
610
- c.http = mock.MagicMock(closed=False)
611
- c.http.ws_connect = mock.AsyncMock(
692
+ http = mock.MagicMock(closed=False)
693
+ http.ws_connect = mock.AsyncMock(
612
694
  side_effect=[aiohttp.client_exceptions.ServerConnectionError()]
613
695
  )
696
+ c = async_client.AsyncClient(http_session=lambda: http)
614
697
  c.sid = '123'
615
698
  assert not await c.connect('http://foo', transports=['websocket'])
616
- c.http.ws_connect.assert_awaited_once_with(
699
+ http.ws_connect.assert_awaited_once_with(
617
700
  'ws://foo/engine.io/?transport=websocket&EIO=4&sid=123&t=123.456',
618
701
  headers={},
619
702
  timeout=5,
620
703
  )
621
704
 
622
705
  async def test_websocket_connection_no_open_packet(self):
623
- c = async_client.AsyncClient()
624
- c.http = mock.MagicMock(closed=False)
625
- c.http.ws_connect = mock.AsyncMock()
626
- ws = c.http.ws_connect.return_value
706
+ http = mock.MagicMock(closed=False)
707
+ http.ws_connect = mock.AsyncMock()
708
+ ws = http.ws_connect.return_value
627
709
  ws.receive = mock.AsyncMock()
628
710
  ws.receive.return_value.data = packet.Packet(
629
711
  packet.CLOSE
630
712
  ).encode()
713
+ c = async_client.AsyncClient(http_session=lambda: http)
631
714
  with pytest.raises(exceptions.ConnectionError):
632
715
  await c.connect('http://foo', transports=['websocket'])
633
716
 
@@ -635,10 +718,9 @@ class TestAsyncClient:
635
718
  @mock.patch('engineio.async_client.aiohttp.ClientWSTimeout',
636
719
  new=mock_ws_timeout)
637
720
  async def test_websocket_connection_successful(self, _time):
638
- c = async_client.AsyncClient()
639
- c.http = mock.MagicMock(closed=False)
640
- c.http.ws_connect = mock.AsyncMock()
641
- ws = c.http.ws_connect.return_value
721
+ http = mock.MagicMock(closed=False)
722
+ http.ws_connect = mock.AsyncMock()
723
+ ws = http.ws_connect.return_value
642
724
  ws.receive = mock.AsyncMock()
643
725
  ws.receive.return_value.data = packet.Packet(
644
726
  packet.OPEN,
@@ -649,6 +731,7 @@ class TestAsyncClient:
649
731
  'pingTimeout': 2000,
650
732
  },
651
733
  ).encode()
734
+ c = async_client.AsyncClient(http_session=lambda: http)
652
735
  c._read_loop_polling = mock.AsyncMock()
653
736
  c._read_loop_websocket = mock.AsyncMock()
654
737
  c._write_loop = mock.AsyncMock()
@@ -668,7 +751,7 @@ class TestAsyncClient:
668
751
  assert c.upgrades == []
669
752
  assert c.transport() == 'websocket'
670
753
  assert c.ws == ws
671
- c.http.ws_connect.assert_awaited_once_with(
754
+ http.ws_connect.assert_awaited_once_with(
672
755
  'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456',
673
756
  headers={},
674
757
  timeout=5,
@@ -676,10 +759,9 @@ class TestAsyncClient:
676
759
 
677
760
  @mock.patch('engineio.client.time.time', return_value=123.456)
678
761
  async def test_websocket_https_noverify_connection_successful(self, _time):
679
- c = async_client.AsyncClient(ssl_verify=False)
680
- c.http = mock.MagicMock(closed=False)
681
- c.http.ws_connect = mock.AsyncMock()
682
- ws = c.http.ws_connect.return_value
762
+ http = mock.MagicMock(closed=False)
763
+ http.ws_connect = mock.AsyncMock()
764
+ ws = http.ws_connect.return_value
683
765
  ws.receive = mock.AsyncMock()
684
766
  ws.receive.return_value.data = packet.Packet(
685
767
  packet.OPEN,
@@ -690,6 +772,8 @@ class TestAsyncClient:
690
772
  'pingTimeout': 2000,
691
773
  },
692
774
  ).encode()
775
+ c = async_client.AsyncClient(
776
+ http_session=lambda: http, ssl_verify=False)
693
777
  c._read_loop_polling = mock.AsyncMock()
694
778
  c._read_loop_websocket = mock.AsyncMock()
695
779
  c._write_loop = mock.AsyncMock()
@@ -709,7 +793,7 @@ class TestAsyncClient:
709
793
  assert c.upgrades == []
710
794
  assert c.transport() == 'websocket'
711
795
  assert c.ws == ws
712
- _, kwargs = c.http.ws_connect.await_args
796
+ _, kwargs = http.ws_connect.await_args
713
797
  assert 'ssl' in kwargs
714
798
  assert isinstance(kwargs['ssl'], ssl.SSLContext)
715
799
  assert kwargs['ssl'].verify_mode == ssl.CERT_NONE
@@ -718,10 +802,9 @@ class TestAsyncClient:
718
802
  @mock.patch('engineio.async_client.aiohttp.ClientWSTimeout',
719
803
  new=mock_ws_timeout)
720
804
  async def test_websocket_connection_with_cookies(self, _time):
721
- c = async_client.AsyncClient()
722
- c.http = mock.MagicMock(closed=False)
723
- c.http.ws_connect = mock.AsyncMock()
724
- ws = c.http.ws_connect.return_value
805
+ http = mock.MagicMock(closed=False)
806
+ http.ws_connect = mock.AsyncMock()
807
+ ws = http.ws_connect.return_value
725
808
  ws.receive = mock.AsyncMock()
726
809
  ws.receive.return_value.data = packet.Packet(
727
810
  packet.OPEN,
@@ -732,18 +815,19 @@ class TestAsyncClient:
732
815
  'pingTimeout': 2000,
733
816
  },
734
817
  ).encode()
735
- c.http._cookie_jar = [mock.MagicMock(), mock.MagicMock()]
736
- c.http._cookie_jar[0].key = 'key'
737
- c.http._cookie_jar[0].value = 'value'
738
- c.http._cookie_jar[1].key = 'key2'
739
- c.http._cookie_jar[1].value = 'value2'
818
+ http._cookie_jar = [mock.MagicMock(), mock.MagicMock()]
819
+ http._cookie_jar[0].key = 'key'
820
+ http._cookie_jar[0].value = 'value'
821
+ http._cookie_jar[1].key = 'key2'
822
+ http._cookie_jar[1].value = 'value2'
823
+ c = async_client.AsyncClient(http_session=lambda: http)
740
824
  c._read_loop_polling = mock.AsyncMock()
741
825
  c._read_loop_websocket = mock.AsyncMock()
742
826
  c._write_loop = mock.AsyncMock()
743
827
  on_connect = mock.MagicMock()
744
828
  c.on('connect', on_connect)
745
829
  await c.connect('ws://foo', transports=['websocket'])
746
- c.http.ws_connect.assert_awaited_once_with(
830
+ http.ws_connect.assert_awaited_once_with(
747
831
  'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456',
748
832
  headers={},
749
833
  timeout=5,
@@ -753,10 +837,9 @@ class TestAsyncClient:
753
837
  @mock.patch('engineio.async_client.aiohttp.ClientWSTimeout',
754
838
  new=mock_ws_timeout)
755
839
  async def test_websocket_connection_with_cookie_header(self, _time):
756
- c = async_client.AsyncClient()
757
- c.http = mock.MagicMock(closed=False)
758
- c.http.ws_connect = mock.AsyncMock()
759
- ws = c.http.ws_connect.return_value
840
+ http = mock.MagicMock(closed=False)
841
+ http.ws_connect = mock.AsyncMock()
842
+ ws = http.ws_connect.return_value
760
843
  ws.receive = mock.AsyncMock()
761
844
  ws.receive.return_value.data = packet.Packet(
762
845
  packet.OPEN,
@@ -767,7 +850,8 @@ class TestAsyncClient:
767
850
  'pingTimeout': 2000,
768
851
  },
769
852
  ).encode()
770
- c.http._cookie_jar = []
853
+ http._cookie_jar = []
854
+ c = async_client.AsyncClient(http_session=lambda: http)
771
855
  c._read_loop_polling = mock.AsyncMock()
772
856
  c._read_loop_websocket = mock.AsyncMock()
773
857
  c._write_loop = mock.AsyncMock()
@@ -778,12 +862,12 @@ class TestAsyncClient:
778
862
  headers={'Cookie': 'key=value; key2=value2; key3="value3="'},
779
863
  transports=['websocket'],
780
864
  )
781
- c.http.ws_connect.assert_awaited_once_with(
865
+ http.ws_connect.assert_awaited_once_with(
782
866
  'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456',
783
867
  headers={},
784
868
  timeout=5,
785
869
  )
786
- c.http.cookie_jar.update_cookies.assert_called_once_with(
870
+ http.cookie_jar.update_cookies.assert_called_once_with(
787
871
  {'key': 'value', 'key2': 'value2', 'key3': 'value3='}
788
872
  )
789
873
 
@@ -791,10 +875,9 @@ class TestAsyncClient:
791
875
  @mock.patch('engineio.async_client.aiohttp.ClientWSTimeout',
792
876
  new=mock_ws_timeout)
793
877
  async def test_websocket_connection_with_cookies_and_headers(self, _time):
794
- c = async_client.AsyncClient()
795
- c.http = mock.MagicMock(closed=False)
796
- c.http.ws_connect = mock.AsyncMock()
797
- ws = c.http.ws_connect.return_value
878
+ http = mock.MagicMock(closed=False)
879
+ http.ws_connect = mock.AsyncMock()
880
+ ws = http.ws_connect.return_value
798
881
  ws.receive = mock.AsyncMock()
799
882
  ws.receive.return_value.data = packet.Packet(
800
883
  packet.OPEN,
@@ -805,11 +888,12 @@ class TestAsyncClient:
805
888
  'pingTimeout': 2000,
806
889
  },
807
890
  ).encode()
808
- c.http._cookie_jar = [mock.MagicMock(), mock.MagicMock()]
809
- c.http._cookie_jar[0].key = 'key'
810
- c.http._cookie_jar[0].value = 'value'
811
- c.http._cookie_jar[1].key = 'key2'
812
- c.http._cookie_jar[1].value = 'value2'
891
+ http._cookie_jar = [mock.MagicMock(), mock.MagicMock()]
892
+ http._cookie_jar[0].key = 'key'
893
+ http._cookie_jar[0].value = 'value'
894
+ http._cookie_jar[1].key = 'key2'
895
+ http._cookie_jar[1].value = 'value2'
896
+ c = async_client.AsyncClient(http_session=lambda: http)
813
897
  c._read_loop_polling = mock.AsyncMock()
814
898
  c._read_loop_websocket = mock.AsyncMock()
815
899
  c._write_loop = mock.AsyncMock()
@@ -820,20 +904,19 @@ class TestAsyncClient:
820
904
  headers={'Foo': 'Bar', 'Cookie': 'key3=value3'},
821
905
  transports=['websocket'],
822
906
  )
823
- c.http.ws_connect.assert_awaited_once_with(
907
+ http.ws_connect.assert_awaited_once_with(
824
908
  'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456',
825
909
  headers={'Foo': 'Bar'},
826
910
  timeout=5,
827
911
  )
828
- c.http.cookie_jar.update_cookies.assert_called_once_with(
912
+ http.cookie_jar.update_cookies.assert_called_once_with(
829
913
  {'key3': 'value3'}
830
914
  )
831
915
 
832
916
  async def test_websocket_upgrade_no_pong(self):
833
- c = async_client.AsyncClient()
834
- c.http = mock.MagicMock(closed=False)
835
- c.http.ws_connect = mock.AsyncMock()
836
- ws = c.http.ws_connect.return_value
917
+ http = mock.MagicMock(closed=False)
918
+ http.ws_connect = mock.AsyncMock()
919
+ ws = http.ws_connect.return_value
837
920
  ws.receive = mock.AsyncMock()
838
921
  ws.receive.return_value.data = packet.Packet(
839
922
  packet.OPEN,
@@ -845,6 +928,7 @@ class TestAsyncClient:
845
928
  },
846
929
  ).encode()
847
930
  ws.send_str = mock.AsyncMock()
931
+ c = async_client.AsyncClient(http_session=lambda: http)
848
932
  c.sid = '123'
849
933
  c.current_transport = 'polling'
850
934
  c._read_loop_polling = mock.AsyncMock()
@@ -862,15 +946,15 @@ class TestAsyncClient:
862
946
  ws.send_str.assert_awaited_once_with('2probe')
863
947
 
864
948
  async def test_websocket_upgrade_successful(self):
865
- c = async_client.AsyncClient()
866
- c.http = mock.MagicMock(closed=False)
867
- c.http.ws_connect = mock.AsyncMock()
868
- ws = c.http.ws_connect.return_value
949
+ http = mock.MagicMock(closed=False)
950
+ http.ws_connect = mock.AsyncMock()
951
+ ws = http.ws_connect.return_value
869
952
  ws.receive = mock.AsyncMock()
870
953
  ws.receive.return_value.data = packet.Packet(
871
954
  packet.PONG, 'probe'
872
955
  ).encode()
873
956
  ws.send_str = mock.AsyncMock()
957
+ c = async_client.AsyncClient(http_session=lambda: http)
874
958
  c.sid = '123'
875
959
  c.base_url = 'http://foo'
876
960
  c.current_transport = 'polling'
@@ -716,6 +716,18 @@ class TestAsyncServer:
716
716
  assert len(s.sockets) == 0
717
717
  assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST'
718
718
 
719
+ @mock.patch('importlib.import_module')
720
+ async def test_post_request_after_upgrade(self, import_module):
721
+ a = self.get_async_mock(
722
+ {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'}
723
+ )
724
+ import_module.side_effect = [a]
725
+ s = async_server.AsyncServer()
726
+ s.sockets['foo'] = self._get_mock_socket()
727
+ s.sockets['foo'].upgraded = True
728
+ await s.handle_request('request')
729
+ assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST'
730
+
719
731
  @mock.patch('importlib.import_module')
720
732
  async def test_send(self, import_module):
721
733
  a = self.get_async_mock()
@@ -44,6 +44,17 @@ class TestClient:
44
44
  assert c.queue.__class__.__name__ == 'Queue'
45
45
  assert c.queue_empty.__name__ == 'Empty'
46
46
 
47
+ def test_external_session(self):
48
+ c = client.Client(http_session='foo')
49
+ assert c.http == 'foo'
50
+ assert c.external_http
51
+
52
+ def test_callable_session(self):
53
+ c = client.Client(http_session=lambda: 'foo')
54
+ assert c.http is None
55
+ assert c.http_factory() == 'foo'
56
+ assert c.external_http is False
57
+
47
58
  def test_custom_json(self):
48
59
  client.Client()
49
60
  assert packet.Packet.json == json
@@ -352,6 +363,22 @@ class TestClient:
352
363
  e.set()
353
364
  assert e.is_set()
354
365
 
366
+ @mock.patch('engineio.client.requests.Session')
367
+ def test_polling_connection_default_session(self, _session):
368
+ _session.return_value.request.return_value.status_code = 200
369
+ _session.return_value.request.return_value.content = payload.Payload(
370
+ packets=[
371
+ packet.Packet(
372
+ packet.MESSAGE, # incorrect on purpose
373
+ {},
374
+ )
375
+ ]
376
+ ).encode().encode('utf-8')
377
+ c = client.Client()
378
+ with pytest.raises(exceptions.ConnectionError,
379
+ match='OPEN packet not returned by server'):
380
+ c.connect('http://foo', headers={'Foo': 'Bar'})
381
+
355
382
  @mock.patch('engineio.client.time.time', return_value=123.456)
356
383
  @mock.patch('engineio.client.Client._send_request', return_value=None)
357
384
  def test_polling_connection_failed(self, _send_request, _time):
@@ -796,7 +823,7 @@ class TestClient:
796
823
  http.auth = None
797
824
  http.proxies = None
798
825
  http.cert = None
799
- c = client.Client(http_session=http)
826
+ c = client.Client(http_session=lambda: http)
800
827
  c._read_loop_polling = mock.MagicMock()
801
828
  c._read_loop_websocket = mock.MagicMock()
802
829
  c._write_loop = mock.MagicMock()
@@ -829,7 +856,7 @@ class TestClient:
829
856
  http.auth = None
830
857
  http.proxies = None
831
858
  http.cert = None
832
- c = client.Client(http_session=http)
859
+ c = client.Client(http_session=lambda: http)
833
860
  c._read_loop_polling = mock.MagicMock()
834
861
  c._read_loop_websocket = mock.MagicMock()
835
862
  c._write_loop = mock.MagicMock()
@@ -872,7 +899,7 @@ class TestClient:
872
899
  http.auth = None
873
900
  http.proxies = None
874
901
  http.cert = None
875
- c = client.Client(http_session=http)
902
+ c = client.Client(http_session=lambda: http)
876
903
  c._read_loop_polling = mock.MagicMock()
877
904
  c._read_loop_websocket = mock.MagicMock()
878
905
  c._write_loop = mock.MagicMock()
@@ -909,7 +936,7 @@ class TestClient:
909
936
  http.auth = ('foo', 'bar')
910
937
  http.proxies = None
911
938
  http.cert = None
912
- c = client.Client(http_session=http)
939
+ c = client.Client(http_session=lambda: http)
913
940
  c._read_loop_polling = mock.MagicMock()
914
941
  c._read_loop_websocket = mock.MagicMock()
915
942
  c._write_loop = mock.MagicMock()
@@ -941,7 +968,7 @@ class TestClient:
941
968
  http.auth = None
942
969
  http.proxies = None
943
970
  http.cert = 'foo.crt'
944
- c = client.Client(http_session=http, ssl_verify=False)
971
+ c = client.Client(http_session=lambda: http, ssl_verify=False)
945
972
  c._read_loop_polling = mock.MagicMock()
946
973
  c._read_loop_websocket = mock.MagicMock()
947
974
  c._write_loop = mock.MagicMock()
@@ -974,7 +1001,7 @@ class TestClient:
974
1001
  http.auth = None
975
1002
  http.proxies = None
976
1003
  http.cert = ('foo.crt', 'key.pem')
977
- c = client.Client(http_session=http)
1004
+ c = client.Client(http_session=lambda: http)
978
1005
  c._read_loop_polling = mock.MagicMock()
979
1006
  c._read_loop_websocket = mock.MagicMock()
980
1007
  c._write_loop = mock.MagicMock()
@@ -1010,7 +1037,7 @@ class TestClient:
1010
1037
  http.proxies = None
1011
1038
  http.cert = ('foo.crt', 'key.pem')
1012
1039
  http.verify = 'ca-bundle.crt'
1013
- c = client.Client(http_session=http)
1040
+ c = client.Client(http_session=lambda: http)
1014
1041
  c._read_loop_polling = mock.MagicMock()
1015
1042
  c._read_loop_websocket = mock.MagicMock()
1016
1043
  c._write_loop = mock.MagicMock()
@@ -1076,7 +1103,7 @@ class TestClient:
1076
1103
  http.auth = None
1077
1104
  http.proxies = proxies
1078
1105
  http.cert = None
1079
- c = client.Client(http_session=http)
1106
+ c = client.Client(http_session=lambda: http)
1080
1107
  c._read_loop_polling = mock.MagicMock()
1081
1108
  c._read_loop_websocket = mock.MagicMock()
1082
1109
  c._write_loop = mock.MagicMock()
@@ -1115,7 +1142,7 @@ class TestClient:
1115
1142
  http.proxies = None
1116
1143
  http.cert = None
1117
1144
  http.verify = False
1118
- c = client.Client(http_session=http)
1145
+ c = client.Client(http_session=lambda: http)
1119
1146
  c._read_loop_polling = mock.MagicMock()
1120
1147
  c._read_loop_websocket = mock.MagicMock()
1121
1148
  c._write_loop = mock.MagicMock()
@@ -1149,7 +1176,7 @@ class TestClient:
1149
1176
  http.proxies = None
1150
1177
  http.cert = None
1151
1178
  http.verify = 'ca-bundle.crt'
1152
- c = client.Client(http_session=http)
1179
+ c = client.Client(http_session=lambda: http)
1153
1180
  c._read_loop_polling = mock.MagicMock()
1154
1181
  c._read_loop_websocket = mock.MagicMock()
1155
1182
  c._write_loop = mock.MagicMock()
@@ -85,6 +85,8 @@ class TestWSGIApp:
85
85
  'Not Found')
86
86
  check_path('/bar/foo', '404 Not Found', 'text/plain', 'Not Found')
87
87
  check_path('', '404 Not Found', 'text/plain', 'Not Found')
88
+ check_path('/static/../../LICENSE', '404 Not Found', 'text/plain',
89
+ 'Not Found')
88
90
 
89
91
  m.static_files[''] = 'index.html'
90
92
  check_path('/static/test/', '200 OK', 'text/html', '<html></html>\n')
@@ -859,6 +859,17 @@ class TestServer:
859
859
  s.handle_request(environ, start_response)
860
860
  assert start_response.call_args[0][0] == '400 BAD REQUEST'
861
861
 
862
+ def test_post_request_after_upgrade(self):
863
+ s = server.Server()
864
+ mock_socket = self._get_mock_socket()
865
+ mock_socket.upgraded = True
866
+ mock_socket.handle_post_request = mock.MagicMock()
867
+ s.sockets['foo'] = mock_socket
868
+ environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'EIO=4&sid=foo'}
869
+ start_response = mock.MagicMock()
870
+ s.handle_request(environ, start_response)
871
+ assert start_response.call_args[0][0] == '400 BAD REQUEST'
872
+
862
873
  def test_send(self):
863
874
  s = server.Server()
864
875
  mock_socket = self._get_mock_socket()
@@ -17,6 +17,12 @@ deps=
17
17
  commands=
18
18
  flake8 --exclude=".*" --ignore=W503,E402,E722 src/engineio tests examples
19
19
 
20
+ [testenv:zizmor]
21
+ deps=
22
+ zizmor
23
+ commands=
24
+ zizmor --offline .forgejo/workflows/tests.yml
25
+
20
26
  [testenv:docs]
21
27
  changedir=docs
22
28
  deps=
@@ -26,3 +32,16 @@ allowlist_externals=
26
32
  make
27
33
  commands=
28
34
  make html
35
+
36
+ [testenv:lint]
37
+ deps=
38
+ flake8
39
+ zizmor
40
+ sphinx
41
+ furo
42
+ allowlist_externals=
43
+ make
44
+ commands=
45
+ flake8 --exclude=".*" --ignore=W503,E402,E722 src/engineio tests examples
46
+ zizmor --offline .forgejo/workflows/tests.yml
47
+ make -C docs html