diff options
| author | Paulo Henrique de Lima Santana (phls) <phls@debian.org> | 2020-12-23 23:27:19 -0300 |
|---|---|---|
| committer | git-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com> | 2020-12-24 10:37:47 +0000 |
| commit | ba80e4edf62f807e4aeb8c676972b15071f80b05 (patch) | |
| tree | 1a1e644afb805cbefd221cf24a0a3a2ce383b305 | |
| parent | 1f767cbb1f3e9fdd34caade8384aca78438ebd55 (diff) | |
4.0.0-1 (patches unapplied)import/4.0.0-1ubuntu/kinetic-develubuntu/kineticubuntu/jammy-develubuntu/jammyubuntu/impish-develubuntu/impishubuntu/hirsute-proposedubuntu/hirsute-develubuntu/hirsutedebian/bullseye
Imported using git-ubuntu import.
Notes
Notes:
* New upstream version 4.0.0.
* debian/control:
- Added Multi-Arch: foreign;
- Bumped Standards-Version to 4.5.1.
* debian/copyright:
- Updated packaging copyright years.
- Updated upstream copyright years.
* debian/manpage: updated manpage with a file from upstream.
* debian/tests/*: updated files to improve tests.
37 files changed, 4182 insertions, 2926 deletions
@@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-engineio -Version: 3.11.1 +Version: 4.0.0 Summary: Engine.IO server Home-page: http://github.com/miguelgrinberg/python-engineio/ Author: Miguel Grinberg @@ -9,8 +9,11 @@ License: MIT Description: python-engineio =============== - .. image:: https://travis-ci.org/miguelgrinberg/python-engineio.svg?branch=master - :target: https://travis-ci.org/miguelgrinberg/python-engineio + .. image:: https://github.com/miguelgrinberg/python-engineio/workflows/build/badge.svg + :target: https://github.com/miguelgrinberg/python-engineio/actions + + .. image:: https://codecov.io/gh/miguelgrinberg/python-engineio/branch/master/graph/badge.svg + :target: https://codecov.io/gh/miguelgrinberg/python-engineio Python implementation of the `Engine.IO`_ realtime client and server. @@ -39,5 +42,5 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules -Provides-Extra: asyncio_client Provides-Extra: client +Provides-Extra: asyncio_client @@ -1,8 +1,11 @@ python-engineio =============== -.. image:: https://travis-ci.org/miguelgrinberg/python-engineio.svg?branch=master - :target: https://travis-ci.org/miguelgrinberg/python-engineio +.. image:: https://github.com/miguelgrinberg/python-engineio/workflows/build/badge.svg + :target: https://github.com/miguelgrinberg/python-engineio/actions + +.. image:: https://codecov.io/gh/miguelgrinberg/python-engineio/branch/master/graph/badge.svg + :target: https://codecov.io/gh/miguelgrinberg/python-engineio Python implementation of the `Engine.IO`_ realtime client and server. diff --git a/debian/changelog b/debian/changelog index 0b217d3..d5701e3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +python-engineio (4.0.0-1) unstable; urgency=medium + + * New upstream version 4.0.0. + * debian/control: + - Added Multi-Arch: foreign; + - Bumped Standards-Version to 4.5.1. + * debian/copyright: + - Updated packaging copyright years. + - Updated upstream copyright years. + * debian/manpage: updated manpage with a file from upstream. + * debian/tests/*: updated files to improve tests. + + -- Paulo Henrique de Lima Santana (phls) <phls@debian.org> Wed, 23 Dec 2020 23:27:19 -0300 + python-engineio (3.11.1-1) unstable; urgency=medium * New upstream version 3.11.1. Consequently fix CVE-2019-13611: websocket diff --git a/debian/control b/debian/control index 995d97a..b3c12d1 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: python-engineio Section: python Priority: optional Maintainer: Paulo Henrique de Lima Santana (phls) <phls@debian.org> -Build-Depends: debhelper-compat (= 12), +Build-Depends: debhelper-compat (= 13), dh-python, python3, python3-aiohttp, @@ -17,7 +17,7 @@ Build-Depends: debhelper-compat (= 12), python3-tornado, python3-websocket, X-Python3-Version: >= 3.7 -Standards-Version: 4.4.1 +Standards-Version: 4.5.1 Rules-Requires-Root: no Homepage: https://pypi.python.org/pypi/python-engineio Vcs-Browser: https://salsa.debian.org/debian/python-engineio diff --git a/debian/copyright b/debian/copyright index 95945a6..98c3abf 100644 --- a/debian/copyright +++ b/debian/copyright @@ -35,13 +35,13 @@ Files: engineio/async_drivers/__init__.py README.rst setup.cfg setup.py -Copyright: 2015-2019, Miguel Grinberg <miguelgrinberg50@gmail.com> +Copyright: 2015-2020, Miguel Grinberg <miguelgrinberg50@gmail.com> License: MIT Comment: Using license as found in LICENSE file. Files: debian/* -Copyright: 2017-2019, Paulo Henrique de Lima Santana (phls) <phls@debian.org> +Copyright: 2017-2020, Paulo Henrique de Lima Santana (phls) <phls@debian.org> License: MIT License: MIT diff --git a/debian/manpage/python-engineio.1 b/debian/manpage/python-engineio.1 index 5136735..06b8ce8 100644 --- a/debian/manpage/python-engineio.1 +++ b/debian/manpage/python-engineio.1 @@ -1,6 +1,6 @@ .\" Man page generated from reStructuredText. . -.TH "PYTHON-ENGINEIO" "1" "Dec 26, 2019" "" "python-engineio" +.TH "PYTHON-ENGINEIO" "1" "Dec 24, 2020" "" "python-engineio" .SH NAME python-engineio \- python-engineio Documentation . @@ -105,7 +105,7 @@ client: .IP \(bu 2 Can connect to other Engine.IO complaint servers besides the one in this package. .IP \(bu 2 -Compatible with Python 2.7 and 3.5+. +Compatible with Python 3.5+. .IP \(bu 2 Two versions of the client, one for standard Python and another for \fBasyncio\fP\&. .IP \(bu 2 @@ -191,7 +191,7 @@ if __name__ == \(aq__main__\(aq: Can accept clients running other complaint Engine.IO clients besides the one in this package. .IP \(bu 2 -Compatible with Python 2.7 and Python 3.5+. +Compatible with Python 3.5+. .IP \(bu 2 Two versions of the server, one for standard Python and another for \fBasyncio\fP\&. .IP \(bu 2 @@ -530,6 +530,33 @@ await eio.sleep(2) .sp The single argument passed to the method is the number of seconds to sleep for. +.SS Debugging and Troubleshooting +.sp +To help you debug issues, the client can be configured to output logs to the +terminal: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +import engineio + +# standard Python +eio = engineio.Client(logger=True) + +# asyncio +eio = engineio.AsyncClient(logger=True) +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +The \fBlogger\fP argument can be set to \fBTrue\fP to output logs to \fBstderr\fP, or +to an object compatible with Python\(aqs \fBlogging\fP package where the logs should +be emitted to. A value of \fBFalse\fP disables logging. +.sp +Logging can help identify the cause of connection problems, unexpected +disconnections and other issues. .SH THE ENGINE.IO SERVER .sp This package contains two Engine.IO servers: @@ -807,6 +834,14 @@ The \fBsid\fP argument passed into all the event handlers is a connection identifier for the client. All the events from a client will use the same \fBsid\fP value. .sp +The \fBconnect\fP handler is the place where the server can perform +authentication. The value returned by this handler is used to determine if the +connection is accepted or rejected. When the handler does not return any value +(which is the same as returning \fBNone\fP) or when it returns \fBTrue\fP the +connection is accepted. If the handler returns \fBFalse\fP or any JSON +compatible data type (string, integer, list or dictionary) the connection is +rejected. A rejected connection triggers a response with a 401 status code. +.sp The \fBdata\fP argument passed to the \fB\(aqmessage\(aq\fP event handler contains application\-specific data provided by the client with the event. .SS Sending Messages @@ -1030,6 +1065,33 @@ await eio.sleep(2) .sp The single argument passed to the method is the number of seconds to sleep for. +.SS Debugging and Troubleshooting +.sp +To help you debug issues, the server can be configured to output logs to the +terminal: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +import engineio + +# standard Python +eio = engineio.Server(logger=True) + +# asyncio +eio = engineio.AsyncServer(logger=True) +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +The \fBlogger\fP argument can be set to \fBTrue\fP to output logs to \fBstderr\fP, or +to an object compatible with Python\(aqs \fBlogging\fP package where the logs should +be emitted to. A value of \fBFalse\fP disables logging. +.sp +Logging can help identify the cause of connection problems, 400 responses, +bad performance and other issues. .SS Deployment Strategies .sp The following sections describe a variety of deployment strategies for @@ -1518,7 +1580,7 @@ attacks. .SS \fBClient\fP class .INDENT 0.0 .TP -.B class engineio.Client(logger=False, json=None, request_timeout=5, ssl_verify=True) +.B class engineio.Client(logger=False, json=None, request_timeout=5, http_session=None, ssl_verify=True) An Engine.IO client. .sp This class implements a fully compliant Engine.IO web client with support @@ -1530,7 +1592,8 @@ for websocket and long\-polling transports. .IP \(bu 2 \fBlogger\fP \-\- To enable logging set to \fBTrue\fP or pass a logger object to use. To disable logging set to \fBFalse\fP\&. The default is -\fBFalse\fP\&. +\fBFalse\fP\&. Note that fatal errors are logged even when +\fBlogger\fP is \fBFalse\fP\&. .IP \(bu 2 \fBjson\fP \-\- An alternative json module to use for encoding and decoding packets. Custom json modules must have \fBdumps\fP and \fBloads\fP @@ -1540,6 +1603,11 @@ versions. \fBrequest_timeout\fP \-\- A timeout in seconds for requests. The default is 5 seconds. .IP \(bu 2 +\fBhttp_session\fP \-\- an initialized \fBrequests.Session\fP object to be used +when sending requests to the server. Use it if you +need to add special client options such as proxy +servers, SSL certificates, etc. +.IP \(bu 2 \fBssl_verify\fP \-\- \fBTrue\fP to verify SSL certificates, or \fBFalse\fP to skip SSL certificate verification, allowing connections to servers with self signed certificates. @@ -1548,7 +1616,7 @@ The default is \fBTrue\fP\&. .UNINDENT .INDENT 7.0 .TP -.B connect(url, headers={}, transports=None, engineio_path=\(aqengine.io\(aq) +.B connect(url, headers=None, transports=None, engineio_path=\(aqengine.io\(aq) Connect to an Engine.IO server. .INDENT 7.0 .TP @@ -1647,22 +1715,14 @@ eio.on(\(aqmessage\(aq, message_handler) .UNINDENT .INDENT 7.0 .TP -.B send(data, binary=None) +.B send(data) Send a message to a client. .INDENT 7.0 .TP .B Parameters -.INDENT 7.0 -.IP \(bu 2 \fBdata\fP \-\- The data to send to the client. Data can be of type \fBstr\fP, \fBbytes\fP, \fBlist\fP or \fBdict\fP\&. If a \fBlist\fP or \fBdict\fP, the data will be serialized as JSON. -.IP \(bu 2 -\fBbinary\fP \-\- \fBTrue\fP to send packet as binary, \fBFalse\fP to send -as text. If not given, unicode (Python 2) and str -(Python 3) are sent as text, and str (Python 2) and -bytes (Python 3) are sent as binary. -.UNINDENT .UNINDENT .UNINDENT .INDENT 7.0 @@ -1714,7 +1774,7 @@ during the life of the connection. .SS \fBAsyncClient\fP class .INDENT 0.0 .TP -.B class engineio.AsyncClient(logger=False, json=None, request_timeout=5, ssl_verify=True) +.B class engineio.AsyncClient(logger=False, json=None, request_timeout=5, http_session=None, ssl_verify=True) An Engine.IO client for asyncio. .sp This class implements a fully compliant Engine.IO web client with support @@ -1727,7 +1787,8 @@ framework on Python 3.5 or newer. .IP \(bu 2 \fBlogger\fP \-\- To enable logging set to \fBTrue\fP or pass a logger object to use. To disable logging set to \fBFalse\fP\&. The default is -\fBFalse\fP\&. +\fBFalse\fP\&. Note that fatal errors are logged even when +\fBlogger\fP is \fBFalse\fP\&. .IP \(bu 2 \fBjson\fP \-\- An alternative json module to use for encoding and decoding packets. Custom json modules must have \fBdumps\fP and \fBloads\fP @@ -1737,6 +1798,11 @@ versions. \fBrequest_timeout\fP \-\- A timeout in seconds for requests. The default is 5 seconds. .IP \(bu 2 +\fBhttp_session\fP \-\- an initialized \fBaiohttp.ClientSession\fP object to be +used when sending requests to the server. Use it if +you need to add special client options such as proxy +servers, SSL certificates, etc. +.IP \(bu 2 \fBssl_verify\fP \-\- \fBTrue\fP to verify SSL certificates, or \fBFalse\fP to skip SSL certificate verification, allowing connections to servers with self signed certificates. @@ -1745,7 +1811,7 @@ The default is \fBTrue\fP\&. .UNINDENT .INDENT 7.0 .TP -.B connect(url, headers={}, transports=None, engineio_path=\(aqengine.io\(aq) +.B async connect(url, headers=None, transports=None, engineio_path=\(aqengine.io\(aq) Connect to an Engine.IO server. .INDENT 7.0 .TP @@ -1796,7 +1862,7 @@ Create a queue object. .UNINDENT .INDENT 7.0 .TP -.B disconnect(abort=False) +.B async disconnect(abort=False) Disconnect from the server. .INDENT 7.0 .TP @@ -1848,29 +1914,21 @@ eio.on(\(aqmessage\(aq, message_handler) .UNINDENT .INDENT 7.0 .TP -.B send(data, binary=None) +.B async send(data) Send a message to a client. .INDENT 7.0 .TP .B Parameters -.INDENT 7.0 -.IP \(bu 2 \fBdata\fP \-\- The data to send to the client. Data can be of type \fBstr\fP, \fBbytes\fP, \fBlist\fP or \fBdict\fP\&. If a \fBlist\fP or \fBdict\fP, the data will be serialized as JSON. -.IP \(bu 2 -\fBbinary\fP \-\- \fBTrue\fP to send packet as binary, \fBFalse\fP to send -as text. If not given, unicode (Python 2) and str -(Python 3) are sent as text, and str (Python 2) and -bytes (Python 3) are sent as binary. -.UNINDENT .UNINDENT .sp Note: this method is a coroutine. .UNINDENT .INDENT 7.0 .TP -.B sleep(seconds=0) +.B async sleep(seconds=0) Sleep for the requested amount of time. .sp Note: this method is a coroutine. @@ -1911,7 +1969,7 @@ The possible values returned by this function are \fB\(aqpolling\(aq\fP and .UNINDENT .INDENT 7.0 .TP -.B wait() +.B async wait() Wait until the connection with the server ends. .sp Client applications can use this function to block the main thread @@ -1923,7 +1981,7 @@ Note: this method is a coroutine. .SS \fBServer\fP class .INDENT 0.0 .TP -.B class engineio.Server(async_mode=None, ping_timeout=60, ping_interval=25, max_http_buffer_size=100000000, allow_upgrades=True, http_compression=True, compression_threshold=1024, cookie=\(aqio\(aq, cors_allowed_origins=None, cors_credentials=True, logger=False, json=None, async_handlers=True, monitor_clients=None, **kwargs) +.B class engineio.Server(async_mode=None, ping_interval=25, ping_timeout=5, max_http_buffer_size=1000000, allow_upgrades=True, http_compression=True, compression_threshold=1024, cookie=None, cors_allowed_origins=None, cors_credentials=True, logger=False, json=None, async_handlers=True, monitor_clients=None, **kwargs) An Engine.IO server. .sp This class implements a fully compliant Engine.IO web server with support @@ -1942,19 +2000,18 @@ argument is not given, "eventlet" is tried first, then The first async mode that has all its dependencies installed is the one that is chosen. .IP \(bu 2 -\fBping_timeout\fP \-\- The time in seconds that the client waits for the -server to respond before disconnecting. The default -is 60 seconds. -.IP \(bu 2 -\fBping_interval\fP \-\- The interval in seconds at which the client pings -the server. The default is 25 seconds. For advanced +\fBping_interval\fP \-\- The interval in seconds at which the server pings +the client. The default is 25 seconds. For advanced control, a two element tuple can be given, where the first number is the ping interval and the second -is a grace period added by the server. The default -grace period is 5 seconds. +is a grace period added by the server. +.IP \(bu 2 +\fBping_timeout\fP \-\- The time in seconds that the client waits for the +server to respond before disconnecting. The default +is 5 seconds. .IP \(bu 2 \fBmax_http_buffer_size\fP \-\- The maximum size of a message when using the -polling transport. The default is 100,000,000 +polling transport. The default is 1,000,000 bytes. .IP \(bu 2 \fBallow_upgrades\fP \-\- Whether to allow transport upgrades or not. The @@ -1967,9 +2024,14 @@ polling transport. The default is \fBTrue\fP\&. is greater than this value. The default is 1024 bytes. .IP \(bu 2 -\fBcookie\fP \-\- Name of the HTTP cookie that contains the client session -id. If set to \fBNone\fP, a cookie is not sent to the client. -The default is \fB\(aqio\(aq\fP\&. +\fBcookie\fP \-\- If set to a string, it is the name of the HTTP cookie the +server sends back tot he client containing the client +session id. If set to a dictionary, the \fB\(aqname\(aq\fP key +contains the cookie name and other keys define cookie +attributes, where the value of each attribute can be a +string, a callable with no arguments, or a boolean. If set +to \fBNone\fP (the default), a cookie is not sent to the +client. .IP \(bu 2 \fBcors_allowed_origins\fP \-\- Origin or list of origins that are allowed to connect to this server. Only the same origin @@ -1983,7 +2045,8 @@ is \fBTrue\fP\&. .IP \(bu 2 \fBlogger\fP \-\- To enable logging set to \fBTrue\fP or pass a logger object to use. To disable logging set to \fBFalse\fP\&. The default is -\fBFalse\fP\&. +\fBFalse\fP\&. Note that fatal errors are logged even when +\fBlogger\fP is \fBFalse\fP\&. .IP \(bu 2 \fBjson\fP \-\- An alternative json module to use for encoding and decoding packets. Custom json modules must have \fBdumps\fP and \fBloads\fP @@ -2034,6 +2097,11 @@ is not given, then all clients are closed. .UNINDENT .INDENT 7.0 .TP +.B generate_id() +Generate a unique session id. +.UNINDENT +.INDENT 7.0 +.TP .B get_queue_empty_exception() Return the queue empty exception for the appropriate async model. .sp @@ -2144,7 +2212,7 @@ Store the user session for a client. .UNINDENT .INDENT 7.0 .TP -.B send(sid, data, binary=None) +.B send(sid, data) Send a message to a client. .INDENT 7.0 .TP @@ -2156,11 +2224,6 @@ Send a message to a client. \fBdata\fP \-\- The data to send to the client. Data can be of type \fBstr\fP, \fBbytes\fP, \fBlist\fP or \fBdict\fP\&. If a \fBlist\fP or \fBdict\fP, the data will be serialized as JSON. -.IP \(bu 2 -\fBbinary\fP \-\- \fBTrue\fP to send packet as binary, \fBFalse\fP to send -as text. If not given, unicode (Python 2) and str -(Python 3) are sent as text, and str (Python 2) and -bytes (Python 3) are sent as binary. .UNINDENT .UNINDENT .UNINDENT @@ -2251,7 +2314,7 @@ and \fB\(aqwebsocket\(aq\fP\&. .SS \fBAsyncServer\fP class .INDENT 0.0 .TP -.B class engineio.AsyncServer(async_mode=None, ping_timeout=60, ping_interval=25, max_http_buffer_size=100000000, allow_upgrades=True, http_compression=True, compression_threshold=1024, cookie=\(aqio\(aq, cors_allowed_origins=None, cors_credentials=True, logger=False, json=None, async_handlers=True, monitor_clients=None, **kwargs) +.B class engineio.AsyncServer(async_mode=None, ping_interval=25, ping_timeout=5, max_http_buffer_size=1000000, allow_upgrades=True, http_compression=True, compression_threshold=1024, cookie=None, cors_allowed_origins=None, cors_credentials=True, logger=False, json=None, async_handlers=True, monitor_clients=None, **kwargs) An Engine.IO server for asyncio. .sp This class implements a fully compliant Engine.IO web server with support @@ -2271,18 +2334,19 @@ given, "aiohttp" is tried first, followed by "sanic", has all its dependencies installed is the one that is chosen. .IP \(bu 2 -\fBping_timeout\fP \-\- The time in seconds that the client waits for the -server to respond before disconnecting. -.IP \(bu 2 -\fBping_interval\fP \-\- The interval in seconds at which the client pings -the server. The default is 25 seconds. For advanced +\fBping_interval\fP \-\- The interval in seconds at which the server pings +the client. The default is 25 seconds. For advanced control, a two element tuple can be given, where the first number is the ping interval and the second -is a grace period added by the server. The default -grace period is 5 seconds. +is a grace period added by the server. +.IP \(bu 2 +\fBping_timeout\fP \-\- The time in seconds that the client waits for the +server to respond before disconnecting. The default +is 5 seconds. .IP \(bu 2 \fBmax_http_buffer_size\fP \-\- The maximum size of a message when using the -polling transport. +polling transport. The default is 1,000,000 +bytes. .IP \(bu 2 \fBallow_upgrades\fP \-\- Whether to allow transport upgrades or not. .IP \(bu 2 @@ -2292,8 +2356,14 @@ polling transport. \fBcompression_threshold\fP \-\- Only compress messages when their byte size is greater than this value. .IP \(bu 2 -\fBcookie\fP \-\- Name of the HTTP cookie that contains the client session -id. If set to \fBNone\fP, a cookie is not sent to the client. +\fBcookie\fP \-\- If set to a string, it is the name of the HTTP cookie the +server sends back tot he client containing the client +session id. If set to a dictionary, the \fB\(aqname\(aq\fP key +contains the cookie name and other keys define cookie +attributes, where the value of each attribute can be a +string, a callable with no arguments, or a boolean. If set +to \fBNone\fP (the default), a cookie is not sent to the +client. .IP \(bu 2 \fBcors_allowed_origins\fP \-\- Origin or list of origins that are allowed to connect to this server. Only the same origin @@ -2305,7 +2375,8 @@ disable CORS handling. allowed in requests to this server. .IP \(bu 2 \fBlogger\fP \-\- To enable logging set to \fBTrue\fP or pass a logger object to -use. To disable logging set to \fBFalse\fP\&. +use. To disable logging set to \fBFalse\fP\&. Note that fatal +errors are logged even when \fBlogger\fP is \fBFalse\fP\&. .IP \(bu 2 \fBjson\fP \-\- An alternative json module to use for encoding and decoding packets. Custom json modules must have \fBdumps\fP and \fBloads\fP @@ -2347,7 +2418,7 @@ async mode. For asyncio based async modes, this returns an instance of .UNINDENT .INDENT 7.0 .TP -.B disconnect(sid=None) +.B async disconnect(sid=None) Disconnect a client. .INDENT 7.0 .TP @@ -2360,6 +2431,11 @@ Note: this method is a coroutine. .UNINDENT .INDENT 7.0 .TP +.B generate_id() +Generate a unique session id. +.UNINDENT +.INDENT 7.0 +.TP .B get_queue_empty_exception() Return the queue empty exception for the appropriate async model. .sp @@ -2370,7 +2446,7 @@ instance of \fBasyncio.QueueEmpty\fP\&. .UNINDENT .INDENT 7.0 .TP -.B get_session(sid) +.B async get_session(sid) Return the user session for a client. .INDENT 7.0 .TP @@ -2384,7 +2460,7 @@ the user session, use the \fBsession\fP context manager instead. .UNINDENT .INDENT 7.0 .TP -.B handle_request(*args, **kwargs) +.B async handle_request(*args, **kwargs) Handle an HTTP request from the client. .sp This is the entry point of the Engine.IO application. This function @@ -2442,7 +2518,7 @@ take a second argument. .UNINDENT .INDENT 7.0 .TP -.B save_session(sid, session) +.B async save_session(sid, session) Store the user session for a client. .INDENT 7.0 .TP @@ -2457,7 +2533,7 @@ Store the user session for a client. .UNINDENT .INDENT 7.0 .TP -.B send(sid, data, binary=None) +.B async send(sid, data) Send a message to a client. .INDENT 7.0 .TP @@ -2469,11 +2545,6 @@ Send a message to a client. \fBdata\fP \-\- The data to send to the client. Data can be of type \fBstr\fP, \fBbytes\fP, \fBlist\fP or \fBdict\fP\&. If a \fBlist\fP or \fBdict\fP, the data will be serialized as JSON. -.IP \(bu 2 -\fBbinary\fP \-\- \fBTrue\fP to send packet as binary, \fBFalse\fP to send -as text. If not given, unicode (Python 2) and str -(Python 3) are sent as text, and str (Python 2) and -bytes (Python 3) are sent as binary. .UNINDENT .UNINDENT .sp @@ -2516,7 +2587,7 @@ def on_message(sid, msg): .UNINDENT .INDENT 7.0 .TP -.B sleep(seconds=0) +.B async sleep(seconds=0) Sleep for the requested amount of time using the appropriate async model. .sp @@ -2615,7 +2686,7 @@ eventlet.wsgi.server(eventlet.listen((\(aq\(aq, 8000)), app) .SS \fBASGIApp\fP class .INDENT 0.0 .TP -.B class engineio.ASGIApp(engineio_server, other_asgi_app=None, static_files=None, engineio_path=\(aqengine.io\(aq) +.B class engineio.ASGIApp(engineio_server, other_asgi_app=None, static_files=None, engineio_path=\(aqengine.io\(aq, on_startup=None, on_shutdown=None) ASGI application middleware for Engine.IO. .sp This middleware dispatches traffic to an Engine.IO application. It can @@ -2637,6 +2708,12 @@ documentation for details on this argument. \fBengineio_path\fP \-\- The endpoint where the Engine.IO application should be installed. The default value is appropriate for most cases. +.IP \(bu 2 +\fBon_startup\fP \-\- function to be called on application startup; can be +coroutine +.IP \(bu 2 +\fBon_shutdown\fP \-\- function to be called on application shutdown; can be +coroutine .UNINDENT .UNINDENT .sp diff --git a/debian/tests/asyncio/test_async_aiohttp.py b/debian/tests/asyncio/test_async_aiohttp.py index ab943aa..f89fb4b 100644 --- a/debian/tests/asyncio/test_async_aiohttp.py +++ b/debian/tests/asyncio/test_async_aiohttp.py @@ -1,17 +1,9 @@ -import sys import unittest +from unittest import mock -import six -if six.PY3: - from unittest import mock -else: - import mock +from engineio.async_drivers import aiohttp as async_aiohttp -if sys.version_info >= (3, 5): - from engineio.async_drivers import aiohttp as async_aiohttp - -@unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') class AiohttpTests(unittest.TestCase): def test_create_route(self): app = mock.MagicMock() @@ -25,9 +17,13 @@ class AiohttpTests(unittest.TestCase): request._message.method = 'PUT' request._message.path = '/foo/bar?baz=1' request._message.version = (1, 1) - request._message.headers = {'a': 'b', 'c-c': 'd', 'c_c': 'e', - 'content-type': 'application/json', - 'content-length': 123} + request._message.headers = { + 'a': 'b', + 'c-c': 'd', + 'c_c': 'e', + 'content-type': 'application/json', + 'content-length': 123, + } request._payload = b'hello world' environ = async_aiohttp.translate_request(request) expected_environ = { @@ -44,14 +40,14 @@ class AiohttpTests(unittest.TestCase): 'aiohttp.request': request, } for k, v in expected_environ.items(): - self.assertEqual(v, environ[k]) - self.assertTrue( - environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d') + assert v == environ[k] + assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' # @mock.patch('async_aiohttp.aiohttp.web.Response') def test_make_response(self): - rv = async_aiohttp.make_response('202 ACCEPTED', {'foo': 'bar'}, - b'payload', {}) - self.assertEqual(rv.status, 202) - self.assertEqual(rv.headers['foo'], 'bar') - self.assertEqual(rv.body, b'payload') + rv = async_aiohttp.make_response( + '202 ACCEPTED', {'foo': 'bar'}, b'payload', {} + ) + assert rv.status == 202 + assert rv.headers['foo'] == 'bar' + assert rv.body == b'payload' diff --git a/debian/tests/asyncio/test_async_asgi.py b/debian/tests/asyncio/test_async_asgi.py index 33e6f10..9559bdb 100644 --- a/debian/tests/asyncio/test_async_asgi.py +++ b/debian/tests/asyncio/test_async_asgi.py @@ -1,16 +1,10 @@ +import asyncio import os import sys import unittest +from unittest import mock -import six -if six.PY3: - from unittest import mock -else: - import mock - -if sys.version_info >= (3, 5): - import asyncio - from engineio.async_drivers import asgi as async_asgi +from engineio.async_drivers import asgi as async_asgi def AsyncMock(*args, **kwargs): @@ -33,12 +27,15 @@ def _run(coro): class AsgiTests(unittest.TestCase): def test_create_app(self): app = async_asgi.ASGIApp( - 'eio', 'other_app', static_files='static_files', - engineio_path='/foo') - self.assertEqual(app.engineio_server, 'eio') - self.assertEqual(app.other_asgi_app, 'other_app') - self.assertEqual(app.static_files, 'static_files') - self.assertEqual(app.engineio_path, 'foo'), + 'eio', + 'other_app', + static_files='static_files', + engineio_path='/foo', + ) + assert app.engineio_server == 'eio' + assert app.other_asgi_app == 'other_app' + assert app.static_files == 'static_files' + assert app.engineio_path == 'foo' def test_engineio_routing(self): mock_server = mock.MagicMock() @@ -47,7 +44,8 @@ class AsgiTests(unittest.TestCase): scope = {'type': 'http', 'path': '/engine.io/'} _run(app(scope, 'receive', 'send')) mock_server.handle_request.mock.assert_called_once_with( - scope, 'receive', 'send') + scope, 'receive', 'send' + ) def test_other_app_routing(self): other_app = AsyncMock() @@ -58,48 +56,58 @@ class AsgiTests(unittest.TestCase): def test_static_file_routing(self): root_dir = os.path.dirname(__file__) - app = async_asgi.ASGIApp('eio', static_files={ - '/': root_dir + '/index.html', - '/foo': {'content_type': 'text/plain', - 'filename': root_dir + '/index.html'}, - '/static': root_dir, - '/static/test/': root_dir + '/', - }) + app = async_asgi.ASGIApp( + 'eio', + static_files={ + '/': root_dir + '/index.html', + '/foo': { + 'content_type': 'text/plain', + 'filename': root_dir + '/index.html', + }, + '/static': root_dir, + '/static/test/': root_dir + '/', + }, + ) def check_path(path, status_code, content_type, body): scope = {'type': 'http', 'path': path} receive = AsyncMock(return_value={'type': 'http.request'}) send = AsyncMock() _run(app(scope, receive, send)) - send.mock.assert_any_call({ - 'type': 'http.response.start', - 'status': status_code, - 'headers': [(b'Content-Type', content_type.encode('utf-8'))]}) - send.mock.assert_any_call({ - 'type': 'http.response.body', - 'body': body.encode('utf-8')}) + send.mock.assert_any_call( + { + 'type': 'http.response.start', + 'status': status_code, + 'headers': [ + (b'Content-Type', content_type.encode('utf-8')) + ], + } + ) + send.mock.assert_any_call( + {'type': 'http.response.body', 'body': body.encode('utf-8')} + ) check_path('/', 200, 'text/html', '<html></html>\n') check_path('/foo', 200, 'text/plain', '<html></html>\n') check_path('/static/index.html', 200, 'text/html', '<html></html>\n') check_path('/static/foo.bar', 404, 'text/plain', 'Not Found') - check_path('/static/test/index.html', 200, 'text/html', - '<html></html>\n') + check_path( + '/static/test/index.html', 200, 'text/html', '<html></html>\n' + ) check_path('/bar/foo', 404, 'text/plain', 'Not Found') check_path('', 404, 'text/plain', 'Not Found') app.static_files[''] = 'index.html' - check_path('/static/test/', 200, 'text/html', - '<html></html>\n') + check_path('/static/test/', 200, 'text/html', '<html></html>\n') app.static_files[''] = {'filename': 'index.html'} - check_path('/static/test/', 200, 'text/html', - '<html></html>\n') + check_path('/static/test/', 200, 'text/html', '<html></html>\n') - app.static_files[''] = {'filename': 'index.html', - 'content_type': 'image/gif'} - check_path('/static/test/', 200, 'image/gif', - '<html></html>\n') + app.static_files[''] = { + 'filename': 'index.html', + 'content_type': 'image/gif', + } + check_path('/static/test/', 200, 'image/gif', '<html></html>\n') app.static_files[''] = {'filename': 'test.gif'} check_path('/static/test/', 404, 'text/plain', 'Not Found') @@ -110,11 +118,63 @@ class AsgiTests(unittest.TestCase): def test_lifespan_startup(self): app = async_asgi.ASGIApp('eio') scope = {'type': 'lifespan'} - receive = AsyncMock(return_value={'type': 'lifespan.startup'}) + receive = AsyncMock(side_effect=[{'type': 'lifespan.startup'}, + {'type': 'lifespan.shutdown'}]) send = AsyncMock() _run(app(scope, receive, send)) - send.mock.assert_called_once_with( - {'type': 'lifespan.startup.complete'}) + send.mock.assert_any_call( + {'type': 'lifespan.startup.complete'} + ) + + def test_lifespan_startup_sync_function(self): + up = False + + def startup(): + nonlocal up + up = True + + app = async_asgi.ASGIApp('eio', on_startup=startup) + scope = {'type': 'lifespan'} + receive = AsyncMock(side_effect=[{'type': 'lifespan.startup'}, + {'type': 'lifespan.shutdown'}]) + send = AsyncMock() + _run(app(scope, receive, send)) + send.mock.assert_any_call( + {'type': 'lifespan.startup.complete'} + ) + assert up + + def test_lifespan_startup_async_function(self): + up = False + + async def startup(): + nonlocal up + up = True + + app = async_asgi.ASGIApp('eio', on_startup=startup) + scope = {'type': 'lifespan'} + receive = AsyncMock(side_effect=[{'type': 'lifespan.startup'}, + {'type': 'lifespan.shutdown'}]) + send = AsyncMock() + _run(app(scope, receive, send)) + send.mock.assert_any_call( + {'type': 'lifespan.startup.complete'} + ) + assert up + + def test_lifespan_startup_function_exception(self): + up = False + + def startup(): + raise Exception + + app = async_asgi.ASGIApp('eio', on_startup=startup) + scope = {'type': 'lifespan'} + receive = AsyncMock(side_effect=[{'type': 'lifespan.startup'}]) + send = AsyncMock() + _run(app(scope, receive, send)) + send.mock.assert_called_once_with({'type': 'lifespan.startup.failed'}) + assert not up def test_lifespan_shutdown(self): app = async_asgi.ASGIApp('eio') @@ -123,15 +183,67 @@ class AsgiTests(unittest.TestCase): send = AsyncMock() _run(app(scope, receive, send)) send.mock.assert_called_once_with( - {'type': 'lifespan.shutdown.complete'}) + {'type': 'lifespan.shutdown.complete'} + ) + + def test_lifespan_shutdown_sync_function(self): + down = False + + def shutdown(): + nonlocal down + down = True + + app = async_asgi.ASGIApp('eio', on_shutdown=shutdown) + scope = {'type': 'lifespan'} + receive = AsyncMock(return_value={'type': 'lifespan.shutdown'}) + send = AsyncMock() + _run(app(scope, receive, send)) + send.mock.assert_called_once_with( + {'type': 'lifespan.shutdown.complete'} + ) + assert down + + def test_lifespan_shutdown_async_function(self): + down = False + + async def shutdown(): + nonlocal down + down = True + + app = async_asgi.ASGIApp('eio', on_shutdown=shutdown) + scope = {'type': 'lifespan'} + receive = AsyncMock(return_value={'type': 'lifespan.shutdown'}) + send = AsyncMock() + _run(app(scope, receive, send)) + send.mock.assert_called_once_with( + {'type': 'lifespan.shutdown.complete'} + ) + assert down + + def test_lifespan_shutdown_function_exception(self): + down = False + + def shutdown(): + raise Exception + + app = async_asgi.ASGIApp('eio', on_shutdown=shutdown) + scope = {'type': 'lifespan'} + receive = AsyncMock(return_value={'type': 'lifespan.shutdown'}) + send = AsyncMock() + _run(app(scope, receive, send)) + send.mock.assert_called_once_with({'type': 'lifespan.shutdown.failed'}) + assert not down def test_lifespan_invalid(self): app = async_asgi.ASGIApp('eio') scope = {'type': 'lifespan'} - receive = AsyncMock(return_value={'type': 'lifespan.foo'}) + receive = AsyncMock(side_effect=[{'type': 'lifespan.foo'}, + {'type': 'lifespan.shutdown'}]) send = AsyncMock() _run(app(scope, receive, send)) - send.mock.assert_not_called() + send.mock.assert_called_once_with( + {'type': 'lifespan.shutdown.complete'} + ) def test_not_found(self): app = async_asgi.ASGIApp('eio') @@ -140,23 +252,40 @@ class AsgiTests(unittest.TestCase): send = AsyncMock() _run(app(scope, receive, send)) send.mock.assert_any_call( - {'type': 'http.response.start', 'status': 404, - 'headers': [(b'Content-Type', b'text/plain')]}) - send.mock.assert_any_call({'type': 'http.response.body', - 'body': b'Not Found'}) + { + 'type': 'http.response.start', + 'status': 404, + 'headers': [(b'Content-Type', b'text/plain')], + } + ) + send.mock.assert_any_call( + {'type': 'http.response.body', 'body': b'Not Found'} + ) def test_translate_request(self): - receive = AsyncMock(return_value={'type': 'http.request', - 'body': b'hello world'}) + receive = AsyncMock( + return_value={'type': 'http.request', 'body': b'hello world'} + ) send = AsyncMock() - environ = _run(async_asgi.translate_request({ - 'type': 'http', - 'method': 'PUT', - 'headers': [(b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), + environ = _run( + async_asgi.translate_request( + { + 'type': 'http', + 'method': 'PUT', + 'headers': [ + (b'a', b'b'), + (b'c-c', b'd'), + (b'c_c', b'e'), (b'content-type', b'application/json'), - (b'content-length', b'123')], - 'path': '/foo/bar', - 'query_string': b'baz=1'}, receive, send)) + (b'content-length', b'123'), + ], + 'path': '/foo/bar', + 'query_string': b'baz=1', + }, + receive, + send, + ) + ) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', @@ -168,26 +297,37 @@ class AsgiTests(unittest.TestCase): 'RAW_URI': '/foo/bar?baz=1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'asgi.receive': receive, - 'asgi.send': send + 'asgi.send': send, } for k, v in expected_environ.items(): - self.assertEqual(v, environ[k]) - self.assertTrue( - environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d') + assert v == environ[k] + assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' body = _run(environ['wsgi.input'].read()) - self.assertEqual(body, b'hello world') + assert body == b'hello world' def test_translate_request_no_query_string(self): - receive = AsyncMock(return_value={'type': 'http.request', - 'body': b'hello world'}) + receive = AsyncMock( + return_value={'type': 'http.request', 'body': b'hello world'} + ) send = AsyncMock() - environ = _run(async_asgi.translate_request({ - 'type': 'http', - 'method': 'PUT', - 'headers': [(b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), + environ = _run( + async_asgi.translate_request( + { + 'type': 'http', + 'method': 'PUT', + 'headers': [ + (b'a', b'b'), + (b'c-c', b'd'), + (b'c_c', b'e'), (b'content-type', b'application/json'), - (b'content-length', b'123')], - 'path': '/foo/bar'}, receive, send)) + (b'content-length', b'123'), + ], + 'path': '/foo/bar', + }, + receive, + send, + ) + ) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', @@ -199,31 +339,43 @@ class AsgiTests(unittest.TestCase): 'RAW_URI': '/foo/bar', 'SERVER_PROTOCOL': 'HTTP/1.1', 'asgi.receive': receive, - 'asgi.send': send + 'asgi.send': send, } for k, v in expected_environ.items(): - self.assertEqual(v, environ[k]) - self.assertTrue( - environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d') + assert v == environ[k] + assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' body = _run(environ['wsgi.input'].read()) - self.assertEqual(body, b'hello world') + assert body == b'hello world' def test_translate_request_with_large_body(self): - receive = AsyncMock(side_effect=[ - {'type': 'http.request', 'body': b'hello ', 'more_body': True}, - {'type': 'http.request', 'body': b'world', 'more_body': True}, - {'type': 'foo.bar'}, # should stop parsing here - {'type': 'http.request', 'body': b'!!!'}, - ]) + receive = AsyncMock( + side_effect=[ + {'type': 'http.request', 'body': b'hello ', 'more_body': True}, + {'type': 'http.request', 'body': b'world', 'more_body': True}, + {'type': 'foo.bar'}, # should stop parsing here + {'type': 'http.request', 'body': b'!!!'}, + ] + ) send = AsyncMock() - environ = _run(async_asgi.translate_request({ - 'type': 'http', - 'method': 'PUT', - 'headers': [(b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), + environ = _run( + async_asgi.translate_request( + { + 'type': 'http', + 'method': 'PUT', + 'headers': [ + (b'a', b'b'), + (b'c-c', b'd'), + (b'c_c', b'e'), (b'content-type', b'application/json'), - (b'content-length', b'123')], - 'path': '/foo/bar', - 'query_string': b'baz=1'}, receive, send)) + (b'content-length', b'123'), + ], + 'path': '/foo/bar', + 'query_string': b'baz=1', + }, + receive, + send, + ) + ) expected_environ = { 'REQUEST_METHOD': 'PUT', 'PATH_INFO': '/foo/bar', @@ -235,45 +387,91 @@ class AsgiTests(unittest.TestCase): 'RAW_URI': '/foo/bar?baz=1', 'SERVER_PROTOCOL': 'HTTP/1.1', 'asgi.receive': receive, - 'asgi.send': send + 'asgi.send': send, } for k, v in expected_environ.items(): - self.assertEqual(v, environ[k]) - self.assertTrue( - environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d') + assert v == environ[k] + assert environ['HTTP_C_C'] == 'd,e' or environ['HTTP_C_C'] == 'e,d' body = _run(environ['wsgi.input'].read()) - self.assertEqual(body, b'hello world') + assert body == b'hello world' def test_translate_websocket_request(self): receive = AsyncMock(return_value={'type': 'websocket.connect'}) send = AsyncMock() - _run(async_asgi.translate_request({ - 'type': 'websocket', - 'headers': [(b'a', b'b'), (b'c-c', b'd'), (b'c_c', b'e'), + _run( + async_asgi.translate_request( + { + 'type': 'websocket', + 'headers': [ + (b'a', b'b'), + (b'c-c', b'd'), + (b'c_c', b'e'), (b'content-type', b'application/json'), - (b'content-length', b'123')], - 'path': '/foo/bar', - 'query_string': b'baz=1'}, receive, send)) - send.mock.assert_called_once_with({'type': 'websocket.accept'}) + (b'content-length', b'123'), + ], + 'path': '/foo/bar', + 'query_string': b'baz=1', + }, + receive, + send, + ) + ) + send.mock.assert_not_called() def test_translate_unknown_request(self): receive = AsyncMock(return_value={'type': 'http.foo'}) send = AsyncMock() - environ = _run(async_asgi.translate_request({ - 'type': 'http', - 'path': '/foo/bar', - 'query_string': b'baz=1'}, receive, send)) - self.assertEqual(environ, {}) + environ = _run( + async_asgi.translate_request( + {'type': 'http', 'path': '/foo/bar', 'query_string': b'baz=1'}, + receive, + send, + ) + ) + assert environ == {} - # @mock.patch('async_aiohttp.aiohttp.web.Response') def test_make_response(self): - environ = { - 'asgi.send': AsyncMock() - } - _run(async_asgi.make_response('202 ACCEPTED', [('foo', 'bar')], - b'payload', environ)) + environ = {'asgi.send': AsyncMock()} + _run( + async_asgi.make_response( + '202 ACCEPTED', [('foo', 'bar')], b'payload', environ + ) + ) environ['asgi.send'].mock.assert_any_call( - {'type': 'http.response.start', 'status': 202, - 'headers': [(b'foo', b'bar')]}) + { + 'type': 'http.response.start', + 'status': 202, + 'headers': [(b'foo', b'bar')], + } + ) environ['asgi.send'].mock.assert_any_call( - {'type': 'http.response.body', 'body': b'payload'}) + {'type': 'http.response.body', 'body': b'payload'} + ) + + def test_make_response_websocket_accept(self): + environ = { + 'asgi.send': AsyncMock(), + 'HTTP_SEC_WEBSOCKET_VERSION': 'foo', + } + _run( + async_asgi.make_response( + '200 OK', [('foo', 'bar')], b'payload', environ + ) + ) + environ['asgi.send'].mock.assert_called_with( + {'type': 'websocket.accept', 'headers': [(b'foo', b'bar')]} + ) + + def test_make_response_websocket_reject(self): + environ = { + 'asgi.send': AsyncMock(), + 'HTTP_SEC_WEBSOCKET_VERSION': 'foo', + } + _run( + async_asgi.make_response( + '401 UNAUTHORIZED', [('foo', 'bar')], b'payload', environ + ) + ) + environ['asgi.send'].mock.assert_called_with( + {'type': 'websocket.close'} + ) diff --git a/debian/tests/asyncio/test_async_sanic.py b/debian/tests/asyncio/test_async_sanic.py new file mode 100644 index 0000000..a1aa7de --- /dev/null +++ b/debian/tests/asyncio/test_async_sanic.py @@ -0,0 +1 @@ +from engineio.async_drivers import sanic as async_sanic # noqa: F401 diff --git a/debian/tests/asyncio/test_async_tornado.py b/debian/tests/asyncio/test_async_tornado.py index dde2186..e6a8a3c 100644 --- a/debian/tests/asyncio/test_async_tornado.py +++ b/debian/tests/asyncio/test_async_tornado.py @@ -1,22 +1,14 @@ -try: - import asyncio -except ImportError: - pass +import asyncio import sys import unittest +from unittest import mock -import six try: import tornado.web except ImportError: pass -if six.PY3: - from unittest import mock -else: - import mock -if sys.version_info >= (3, 5): - from engineio.async_drivers import tornado as async_tornado +from engineio.async_drivers import tornado as async_tornado def _run(coro): @@ -29,8 +21,7 @@ class TornadoTests(unittest.TestCase): def test_get_tornado_handler(self): mock_server = mock.MagicMock() handler = async_tornado.get_tornado_handler(mock_server) - self.assertTrue(issubclass(handler, - tornado.websocket.WebSocketHandler)) + assert issubclass(handler, tornado.websocket.WebSocketHandler) def test_translate_request(self): mock_handler = mock.MagicMock() @@ -42,7 +33,7 @@ class TornadoTests(unittest.TestCase): 'a': 'b', 'c': 'd', 'content-type': 'application/json', - 'content-length': 123 + 'content-length': 123, } mock_handler.request.body = b'hello world' environ = async_tornado.translate_request(mock_handler) @@ -60,16 +51,17 @@ class TornadoTests(unittest.TestCase): 'tornado.handler': mock_handler, } for k, v in expected_environ.items(): - self.assertEqual(v, environ[k]) + assert v == environ[k] payload = _run(environ['wsgi.input'].read(1)) payload += _run(environ['wsgi.input'].read()) - self.assertEqual(payload, b'hello world') + assert payload == b'hello world' def test_make_response(self): mock_handler = mock.MagicMock() mock_environ = {'tornado.handler': mock_handler} - async_tornado.make_response('202 ACCEPTED', [('foo', 'bar')], - b'payload', mock_environ) + async_tornado.make_response( + '202 ACCEPTED', [('foo', 'bar')], b'payload', mock_environ + ) mock_handler.set_status.assert_called_once_with(202) mock_handler.set_header.assert_called_once_with('foo', 'bar') mock_handler.write.assert_called_once_with(b'payload') diff --git a/debian/tests/asyncio/test_asyncio_client.py b/debian/tests/asyncio/test_asyncio_client.py index 236e90d..bea49e1 100644 --- a/debian/tests/asyncio/test_asyncio_client.py +++ b/debian/tests/asyncio/test_asyncio_client.py @@ -1,18 +1,14 @@ import asyncio import ssl import sys -import time import unittest +from unittest import mock -import six -if six.PY3: - from unittest import mock -else: - import mock try: import aiohttp except ImportError: aiohttp = None +import pytest from engineio import asyncio_client from engineio import client @@ -41,79 +37,80 @@ def _run(coro): class TestAsyncClient(unittest.TestCase): def test_is_asyncio_based(self): c = asyncio_client.AsyncClient() - self.assertEqual(c.is_asyncio_based(), True) + assert c.is_asyncio_based() def test_already_connected(self): c = asyncio_client.AsyncClient() c.state = 'connected' - self.assertRaises(ValueError, _run, c.connect('http://foo')) + with pytest.raises(ValueError): + _run(c.connect('http://foo')) def test_invalid_transports(self): c = asyncio_client.AsyncClient() - self.assertRaises(ValueError, _run, c.connect( - 'http://foo', transports=['foo', 'bar'])) + with pytest.raises(ValueError): + _run(c.connect('http://foo', transports=['foo', 'bar'])) def test_some_invalid_transports(self): c = asyncio_client.AsyncClient() c._connect_websocket = AsyncMock() _run(c.connect('http://foo', transports=['foo', 'websocket', 'bar'])) - self.assertEqual(c.transports, ['websocket']) + assert c.transports == ['websocket'] def test_connect_polling(self): c = asyncio_client.AsyncClient() c._connect_polling = AsyncMock(return_value='foo') - self.assertEqual(_run(c.connect('http://foo')), 'foo') + assert _run(c.connect('http://foo')) == 'foo' c._connect_polling.mock.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) c = asyncio_client.AsyncClient() c._connect_polling = AsyncMock(return_value='foo') - self.assertEqual( - _run(c.connect('http://foo', transports=['polling'])), 'foo') + assert _run(c.connect('http://foo', transports=['polling'])) == 'foo' c._connect_polling.mock.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) c = asyncio_client.AsyncClient() c._connect_polling = AsyncMock(return_value='foo') - self.assertEqual( - _run(c.connect('http://foo', transports=['polling', - 'websocket'])), - 'foo') + assert ( + _run(c.connect('http://foo', transports=['polling', 'websocket'])) + == 'foo' + ) c._connect_polling.mock.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) def test_connect_websocket(self): c = asyncio_client.AsyncClient() c._connect_websocket = AsyncMock(return_value='foo') - self.assertEqual( - _run(c.connect('http://foo', transports=['websocket'])), - 'foo') + assert _run(c.connect('http://foo', transports=['websocket'])) == 'foo' c._connect_websocket.mock.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) c = asyncio_client.AsyncClient() c._connect_websocket = AsyncMock(return_value='foo') - self.assertEqual( - _run(c.connect('http://foo', transports='websocket')), - 'foo') + assert _run(c.connect('http://foo', transports='websocket')) == 'foo' c._connect_websocket.mock.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) def test_connect_query_string(self): c = asyncio_client.AsyncClient() c._connect_polling = AsyncMock(return_value='foo') - self.assertEqual(_run(c.connect('http://foo?bar=baz')), 'foo') + assert _run(c.connect('http://foo?bar=baz')) == 'foo' c._connect_polling.mock.assert_called_once_with( - 'http://foo?bar=baz', {}, 'engine.io') + 'http://foo?bar=baz', {}, 'engine.io' + ) def test_connect_custom_headers(self): c = asyncio_client.AsyncClient() c._connect_polling = AsyncMock(return_value='foo') - self.assertEqual( - _run(c.connect('http://foo', headers={'Foo': 'Bar'})), - 'foo') + assert _run(c.connect('http://foo', headers={'Foo': 'Bar'})) == 'foo' c._connect_polling.mock.assert_called_once_with( - 'http://foo', {'Foo': 'Bar'}, 'engine.io') + 'http://foo', {'Foo': 'Bar'}, 'engine.io' + ) def test_wait(self): c = asyncio_client.AsyncClient() @@ -124,7 +121,7 @@ class TestAsyncClient(unittest.TestCase): c.read_loop_task = fake_read_look_task() _run(c.wait()) - self.assertEqual(done, [True]) + assert done == [True] def test_wait_no_task(self): c = asyncio_client.AsyncClient() @@ -140,26 +137,25 @@ class TestAsyncClient(unittest.TestCase): c._send_packet = fake_send_packet _run(c.send('foo')) - _run(c.send('foo', binary=False)) - _run(c.send(b'foo', binary=True)) - self.assertEqual(saved_packets[0].packet_type, packet.MESSAGE) - self.assertEqual(saved_packets[0].data, 'foo') - self.assertEqual(saved_packets[0].binary, - False if six.PY3 else True) - self.assertEqual(saved_packets[1].packet_type, packet.MESSAGE) - self.assertEqual(saved_packets[1].data, 'foo') - self.assertEqual(saved_packets[1].binary, False) - self.assertEqual(saved_packets[2].packet_type, packet.MESSAGE) - self.assertEqual(saved_packets[2].data, b'foo') - self.assertEqual(saved_packets[2].binary, True) + _run(c.send('foo')) + _run(c.send(b'foo')) + assert saved_packets[0].packet_type == packet.MESSAGE + assert saved_packets[0].data == 'foo' + assert not saved_packets[0].binary + assert saved_packets[1].packet_type == packet.MESSAGE + assert saved_packets[1].data == 'foo' + assert not saved_packets[1].binary + assert saved_packets[2].packet_type == packet.MESSAGE + assert saved_packets[2].data == b'foo' + assert saved_packets[2].binary def test_disconnect_not_connected(self): c = asyncio_client.AsyncClient() c.state = 'foo' c.sid = 'bar' _run(c.disconnect()) - self.assertEqual(c.state, 'disconnected') - self.assertIsNone(c.sid) + assert c.state == 'disconnected' + assert c.sid is None def test_disconnect_polling(self): c = asyncio_client.AsyncClient() @@ -175,9 +171,10 @@ class TestAsyncClient(unittest.TestCase): c._trigger_event = AsyncMock() _run(c.disconnect()) c.ws.close.mock.assert_not_called() - self.assertNotIn(c, client.connected_clients) - c._trigger_event.mock.assert_called_once_with('disconnect', - run_async=False) + assert c not in client.connected_clients + c._trigger_event.mock.assert_called_once_with( + 'disconnect', run_async=False + ) def test_disconnect_websocket(self): c = asyncio_client.AsyncClient() @@ -193,9 +190,10 @@ class TestAsyncClient(unittest.TestCase): c._trigger_event = AsyncMock() _run(c.disconnect()) c.ws.close.mock.assert_called_once_with() - self.assertNotIn(c, client.connected_clients) - c._trigger_event.mock.assert_called_once_with('disconnect', - run_async=False) + assert c not in client.connected_clients + c._trigger_event.mock.assert_called_once_with( + 'disconnect', run_async=False + ) def test_disconnect_polling_abort(self): c = asyncio_client.AsyncClient() @@ -211,7 +209,7 @@ class TestAsyncClient(unittest.TestCase): _run(c.disconnect(abort=True)) c.queue.join.mock.assert_not_called() c.ws.close.mock.assert_not_called() - self.assertNotIn(c, client.connected_clients) + assert c not in client.connected_clients def test_disconnect_websocket_abort(self): c = asyncio_client.AsyncClient() @@ -227,7 +225,7 @@ class TestAsyncClient(unittest.TestCase): _run(c.disconnect(abort=True)) c.queue.join.mock.assert_not_called() c.ws.mock.assert_not_called() - self.assertNotIn(c, client.connected_clients) + assert c not in client.connected_clients def test_background_tasks(self): r = [] @@ -237,9 +235,10 @@ class TestAsyncClient(unittest.TestCase): c = asyncio_client.AsyncClient() c.start_background_task(foo, 'bar') - pending = asyncio.Task.all_tasks() + pending = asyncio.all_tasks(loop=asyncio.get_event_loop()) \ + if hasattr(asyncio, 'all_tasks') else asyncio.Task.all_tasks() asyncio.get_event_loop().run_until_complete(asyncio.wait(pending)) - self.assertEqual(r, ['bar']) + assert r == ['bar'] def test_sleep(self): c = asyncio_client.AsyncClient() @@ -248,139 +247,193 @@ class TestAsyncClient(unittest.TestCase): def test_create_queue(self): c = asyncio_client.AsyncClient() q = c.create_queue() - self.assertRaises(q.Empty, q.get_nowait) + with pytest.raises(q.Empty): + q.get_nowait() def test_create_event(self): c = asyncio_client.AsyncClient() e = c.create_event() - self.assertFalse(e.is_set()) + assert not e.is_set() e.set() - self.assertTrue(e.is_set()) + assert e.is_set() @mock.patch('engineio.client.time.time', return_value=123.456) def test_polling_connection_failed(self, _time): c = asyncio_client.AsyncClient() c._send_request = AsyncMock(return_value=None) - self.assertRaises( - exceptions.ConnectionError, _run, c.connect( - 'http://foo', headers={'Foo': 'Bar'})) + with pytest.raises(exceptions.ConnectionError): + _run(c.connect('http://foo', headers={'Foo': 'Bar'})) c._send_request.mock.assert_called_once_with( - 'GET', 'http://foo/engine.io/?transport=polling&EIO=3&t=123.456', - headers={'Foo': 'Bar'}, timeout=5) + 'GET', + 'http://foo/engine.io/?transport=polling&EIO=4&t=123.456', + headers={'Foo': 'Bar'}, + timeout=5, + ) def test_polling_connection_404(self): c = asyncio_client.AsyncClient() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 404 - self.assertRaises( - exceptions.ConnectionError, _run, c.connect('http://foo')) + c._send_request.mock.return_value.json = AsyncMock( + return_value={'foo': 'bar'} + ) + try: + _run(c.connect('http://foo')) + except exceptions.ConnectionError as exc: + assert len(exc.args) == 2 + assert ( + exc.args[0] == 'Unexpected status code 404 in server response' + ) + assert exc.args[1] == {'foo': 'bar'} + + def test_polling_connection_404_no_json(self): + c = asyncio_client.AsyncClient() + c._send_request = AsyncMock() + c._send_request.mock.return_value.status = 404 + c._send_request.mock.return_value.json = AsyncMock( + side_effect=aiohttp.ContentTypeError('foo', 'bar') + ) + try: + _run(c.connect('http://foo')) + except exceptions.ConnectionError as exc: + assert len(exc.args) == 2 + assert ( + exc.args[0] == 'Unexpected status code 404 in server response' + ) + assert exc.args[1] is None def test_polling_connection_invalid_packet(self): c = asyncio_client.AsyncClient() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 - c._send_request.mock.return_value.read = AsyncMock( - return_value=b'foo') - self.assertRaises( - exceptions.ConnectionError, _run, c.connect('http://foo')) + c._send_request.mock.return_value.read = AsyncMock(return_value=b'foo') + with pytest.raises(exceptions.ConnectionError): + _run(c.connect('http://foo')) def test_polling_connection_no_open_packet(self): c = asyncio_client.AsyncClient() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 c._send_request.mock.return_value.read = AsyncMock( - return_value=payload.Payload(packets=[ - packet.Packet(packet.CLOSE, { - 'sid': '123', 'upgrades': [], 'pingInterval': 10, - 'pingTimeout': 20 - }) - ]).encode()) - self.assertRaises( - exceptions.ConnectionError, _run, c.connect('http://foo')) + return_value=payload.Payload( + packets=[ + packet.Packet( + packet.CLOSE, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 10, + 'pingTimeout': 20, + }, + ) + ] + ).encode().encode('utf-8') + ) + with pytest.raises(exceptions.ConnectionError): + _run(c.connect('http://foo')) def test_polling_connection_successful(self): c = asyncio_client.AsyncClient() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 c._send_request.mock.return_value.read = AsyncMock( - return_value=payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }) - ]).encode()) - c._ping_loop = AsyncMock() + return_value=payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') + ) c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = AsyncMock() c.on('connect', on_connect) _run(c.connect('http://foo')) - time.sleep(0.1) - c._ping_loop.mock.assert_called_once_with() c._read_loop_polling.mock.assert_called_once_with() c._read_loop_websocket.mock.assert_not_called() c._write_loop.mock.assert_called_once_with() on_connect.mock.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'http://foo/engine.io/?transport=polling&EIO=3&sid=123') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'polling') + assert c in client.connected_clients + assert ( + c.base_url + == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' + ) + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'polling' def test_polling_https_noverify_connection_successful(self): c = asyncio_client.AsyncClient(ssl_verify=False) c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 c._send_request.mock.return_value.read = AsyncMock( - return_value=payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }) - ]).encode()) - c._ping_loop = AsyncMock() + return_value=payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') + ) c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = AsyncMock() c.on('connect', on_connect) _run(c.connect('https://foo')) - time.sleep(0.1) - c._ping_loop.mock.assert_called_once_with() c._read_loop_polling.mock.assert_called_once_with() c._read_loop_websocket.mock.assert_not_called() c._write_loop.mock.assert_called_once_with() on_connect.mock.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'https://foo/engine.io/?transport=polling&EIO=3&sid=123') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'polling') + assert c in client.connected_clients + assert ( + c.base_url + == 'https://foo/engine.io/?transport=polling&EIO=4&sid=123' + ) + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'polling' def test_polling_connection_with_more_packets(self): c = asyncio_client.AsyncClient() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 c._send_request.mock.return_value.read = AsyncMock( - return_value=payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }), - packet.Packet(packet.NOOP) - ]).encode()) - c._ping_loop = AsyncMock() + return_value=payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ), + packet.Packet(packet.NOOP), + ] + ).encode().encode('utf-8') + ) c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() @@ -388,96 +441,119 @@ class TestAsyncClient(unittest.TestCase): on_connect = AsyncMock() c.on('connect', on_connect) _run(c.connect('http://foo')) - time.sleep(0.1) - self.assertEqual(c._receive_packet.mock.call_count, 1) - self.assertEqual( - c._receive_packet.mock.call_args_list[0][0][0].packet_type, - packet.NOOP) + assert c._receive_packet.mock.call_count == 1 + assert ( + c._receive_packet.mock.call_args_list[0][0][0].packet_type + == packet.NOOP + ) def test_polling_connection_upgraded(self): c = asyncio_client.AsyncClient() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 c._send_request.mock.return_value.read = AsyncMock( - return_value=payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': ['websocket'], - 'pingInterval': 1000, 'pingTimeout': 2000 - }) - ]).encode()) + return_value=payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': ['websocket'], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') + ) c._connect_websocket = AsyncMock(return_value=True) on_connect = mock.MagicMock() c.on('connect', on_connect) _run(c.connect('http://foo')) - c._connect_websocket.mock.assert_called_once_with('http://foo', {}, - 'engine.io') + c._connect_websocket.mock.assert_called_once_with( + 'http://foo', {}, 'engine.io' + ) on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'http://foo/engine.io/?transport=polling&EIO=3&sid=123') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, ['websocket']) + assert c in client.connected_clients + assert ( + c.base_url + == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' + ) + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == ['websocket'] def test_polling_connection_not_upgraded(self): c = asyncio_client.AsyncClient() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 c._send_request.mock.return_value.read = AsyncMock( - return_value=payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': ['websocket'], - 'pingInterval': 1000, 'pingTimeout': 2000 - }) - ]).encode()) + return_value=payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': ['websocket'], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') + ) c._connect_websocket = AsyncMock(return_value=False) - c._ping_loop = AsyncMock() c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) _run(c.connect('http://foo')) - time.sleep(0.1) - c._connect_websocket.mock.assert_called_once_with('http://foo', {}, - 'engine.io') - c._ping_loop.mock.assert_called_once_with() + c._connect_websocket.mock.assert_called_once_with( + 'http://foo', {}, 'engine.io' + ) c._read_loop_polling.mock.assert_called_once_with() c._read_loop_websocket.mock.assert_not_called() c._write_loop.mock.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) + assert c in client.connected_clients @mock.patch('engineio.client.time.time', return_value=123.456) def test_websocket_connection_failed(self, _time): c = asyncio_client.AsyncClient() c.http = mock.MagicMock(closed=False) - c.http.ws_connect = AsyncMock(side_effect=[ - aiohttp.client_exceptions.ServerConnectionError()]) - self.assertRaises( - exceptions.ConnectionError, _run, - c.connect('http://foo', transports=['websocket'], - headers={'Foo': 'Bar'})) + c.http.ws_connect = AsyncMock( + side_effect=[aiohttp.client_exceptions.ServerConnectionError()] + ) + with pytest.raises(exceptions.ConnectionError): + _run( + c.connect( + 'http://foo', + transports=['websocket'], + headers={'Foo': 'Bar'}, + ) + ) c.http.ws_connect.mock.assert_called_once_with( - 'ws://foo/engine.io/?transport=websocket&EIO=3&t=123.456', - headers={'Foo': 'Bar'}) + 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', + headers={'Foo': 'Bar'}, + ) @mock.patch('engineio.client.time.time', return_value=123.456) def test_websocket_upgrade_failed(self, _time): c = asyncio_client.AsyncClient() c.http = mock.MagicMock(closed=False) - c.http.ws_connect = AsyncMock(side_effect=[ - aiohttp.client_exceptions.ServerConnectionError()]) + c.http.ws_connect = AsyncMock( + side_effect=[aiohttp.client_exceptions.ServerConnectionError()] + ) c.sid = '123' - self.assertFalse(_run(c.connect( - 'http://foo', transports=['websocket']))) + assert not _run(c.connect('http://foo', transports=['websocket'])) c.http.ws_connect.mock.assert_called_once_with( - 'ws://foo/engine.io/?transport=websocket&EIO=3&sid=123&t=123.456', - headers={}) + 'ws://foo/engine.io/?transport=websocket&EIO=4&sid=123&t=123.456', + headers={}, + ) def test_websocket_connection_no_open_packet(self): c = asyncio_client.AsyncClient() @@ -486,10 +562,10 @@ class TestAsyncClient(unittest.TestCase): ws = c.http.ws_connect.mock.return_value ws.receive = AsyncMock() ws.receive.mock.return_value.data = packet.Packet( - packet.CLOSE).encode() - self.assertRaises( - exceptions.ConnectionError, _run, - c.connect('http://foo', transports=['websocket'])) + packet.CLOSE + ).encode() + with pytest.raises(exceptions.ConnectionError): + _run(c.connect('http://foo', transports=['websocket'])) @mock.patch('engineio.client.time.time', return_value=123.456) def test_websocket_connection_successful(self, _time): @@ -498,37 +574,38 @@ class TestAsyncClient(unittest.TestCase): c.http.ws_connect = AsyncMock() ws = c.http.ws_connect.mock.return_value ws.receive = AsyncMock() - ws.receive.mock.return_value.data = packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() - c._ping_loop = AsyncMock() + ws.receive.mock.return_value.data = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) _run(c.connect('ws://foo', transports=['websocket'])) - time.sleep(0.1) - c._ping_loop.mock.assert_called_once_with() c._read_loop_polling.mock.assert_not_called() c._read_loop_websocket.mock.assert_called_once_with() c._write_loop.mock.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'ws://foo/engine.io/?transport=websocket&EIO=3') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'websocket') - self.assertEqual(c.ws, ws) + assert c in client.connected_clients + assert c.base_url == 'ws://foo/engine.io/?transport=websocket&EIO=4' + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'websocket' + assert c.ws == ws c.http.ws_connect.mock.assert_called_once_with( - 'ws://foo/engine.io/?transport=websocket&EIO=3&t=123.456', - headers={}) + 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', + headers={}, + ) @mock.patch('engineio.client.time.time', return_value=123.456) def test_websocket_https_noverify_connection_successful(self, _time): @@ -537,38 +614,38 @@ class TestAsyncClient(unittest.TestCase): c.http.ws_connect = AsyncMock() ws = c.http.ws_connect.mock.return_value ws.receive = AsyncMock() - ws.receive.mock.return_value.data = packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() - c._ping_loop = AsyncMock() + ws.receive.mock.return_value.data = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) _run(c.connect('wss://foo', transports=['websocket'])) - time.sleep(0.1) - c._ping_loop.mock.assert_called_once_with() c._read_loop_polling.mock.assert_not_called() c._read_loop_websocket.mock.assert_called_once_with() c._write_loop.mock.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'wss://foo/engine.io/?transport=websocket&EIO=3') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'websocket') - self.assertEqual(c.ws, ws) + assert c in client.connected_clients + assert c.base_url == 'wss://foo/engine.io/?transport=websocket&EIO=4' + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'websocket' + assert c.ws == ws _, kwargs = c.http.ws_connect.mock.call_args - self.assertTrue('ssl' in kwargs) - self.assertTrue(isinstance(kwargs['ssl'], ssl.SSLContext)) - self.assertEqual(kwargs['ssl'].verify_mode, ssl.CERT_NONE) + assert 'ssl' in kwargs + assert isinstance(kwargs['ssl'], ssl.SSLContext) + assert kwargs['ssl'].verify_mode == ssl.CERT_NONE @mock.patch('engineio.client.time.time', return_value=123.456) def test_websocket_connection_with_cookies(self, _time): @@ -577,26 +654,108 @@ class TestAsyncClient(unittest.TestCase): c.http.ws_connect = AsyncMock() ws = c.http.ws_connect.mock.return_value ws.receive = AsyncMock() - ws.receive.mock.return_value.data = packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() + ws.receive.mock.return_value.data = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() c.http._cookie_jar = [mock.MagicMock(), mock.MagicMock()] c.http._cookie_jar[0].key = 'key' c.http._cookie_jar[0].value = 'value' c.http._cookie_jar[1].key = 'key2' c.http._cookie_jar[1].value = 'value2' - c._ping_loop = AsyncMock() c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) _run(c.connect('ws://foo', transports=['websocket'])) - time.sleep(0.1) c.http.ws_connect.mock.assert_called_once_with( - 'ws://foo/engine.io/?transport=websocket&EIO=3&t=123.456', - headers={}) + 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', + headers={}, + ) + + @mock.patch('engineio.client.time.time', return_value=123.456) + def test_websocket_connection_with_cookie_header(self, _time): + c = asyncio_client.AsyncClient() + c.http = mock.MagicMock(closed=False) + c.http.ws_connect = AsyncMock() + ws = c.http.ws_connect.mock.return_value + ws.receive = AsyncMock() + ws.receive.mock.return_value.data = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + c.http._cookie_jar = [] + c._read_loop_polling = AsyncMock() + c._read_loop_websocket = AsyncMock() + c._write_loop = AsyncMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + _run( + c.connect( + 'ws://foo', + headers={'Cookie': 'key=value; key2=value2; key3="value3="'}, + transports=['websocket'], + ) + ) + c.http.ws_connect.mock.assert_called_once_with( + 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', + headers={}, + ) + c.http.cookie_jar.update_cookies.assert_called_once_with( + {'key': 'value', 'key2': 'value2', 'key3': '"value3="'} + ) + + @mock.patch('engineio.client.time.time', return_value=123.456) + def test_websocket_connection_with_cookies_and_headers(self, _time): + c = asyncio_client.AsyncClient() + c.http = mock.MagicMock(closed=False) + c.http.ws_connect = AsyncMock() + ws = c.http.ws_connect.mock.return_value + ws.receive = AsyncMock() + ws.receive.mock.return_value.data = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + c.http._cookie_jar = [mock.MagicMock(), mock.MagicMock()] + c.http._cookie_jar[0].key = 'key' + c.http._cookie_jar[0].value = 'value' + c.http._cookie_jar[1].key = 'key2' + c.http._cookie_jar[1].value = 'value2' + c._read_loop_polling = AsyncMock() + c._read_loop_websocket = AsyncMock() + c._write_loop = AsyncMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + _run( + c.connect( + 'ws://foo', + headers={'Foo': 'Bar', 'Cookie': 'key3=value3'}, + transports=['websocket'], + ) + ) + c.http.ws_connect.mock.assert_called_once_with( + 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', + headers={'Foo': 'Bar'}, + ) + c.http.cookie_jar.update_cookies.assert_called_once_with( + {'key3': 'value3'} + ) def test_websocket_upgrade_no_pong(self): c = asyncio_client.AsyncClient() @@ -604,28 +763,30 @@ class TestAsyncClient(unittest.TestCase): c.http.ws_connect = AsyncMock() ws = c.http.ws_connect.mock.return_value ws.receive = AsyncMock() - ws.receive.mock.return_value.data = packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() + ws.receive.mock.return_value.data = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() ws.send_str = AsyncMock() c.sid = '123' c.current_transport = 'polling' - c._ping_loop = AsyncMock() c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) - self.assertFalse(_run(c.connect('ws://foo', - transports=['websocket']))) + assert not _run(c.connect('ws://foo', transports=['websocket'])) - c._ping_loop.mock.assert_not_called() c._read_loop_polling.mock.assert_not_called() c._read_loop_websocket.mock.assert_not_called() c._write_loop.mock.assert_not_called() on_connect.assert_not_called() - self.assertEqual(c.transport(), 'polling') + assert c.transport() == 'polling' ws.send_str.mock.assert_called_once_with('2probe') def test_websocket_upgrade_successful(self): @@ -635,41 +796,34 @@ class TestAsyncClient(unittest.TestCase): ws = c.http.ws_connect.mock.return_value ws.receive = AsyncMock() ws.receive.mock.return_value.data = packet.Packet( - packet.PONG, 'probe').encode() + packet.PONG, 'probe' + ).encode() ws.send_str = AsyncMock() c.sid = '123' c.base_url = 'http://foo' c.current_transport = 'polling' - c._ping_loop = AsyncMock() c._read_loop_polling = AsyncMock() c._read_loop_websocket = AsyncMock() c._write_loop = AsyncMock() on_connect = mock.MagicMock() c.on('connect', on_connect) - self.assertTrue(_run(c.connect('ws://foo', - transports=['websocket']))) - time.sleep(0.1) + assert _run(c.connect('ws://foo', transports=['websocket'])) - c._ping_loop.mock.assert_called_once_with() c._read_loop_polling.mock.assert_not_called() c._read_loop_websocket.mock.assert_called_once_with() c._write_loop.mock.assert_called_once_with() on_connect.assert_not_called() # was called by polling - self.assertNotIn(c, client.connected_clients) # was added by polling - self.assertEqual(c.base_url, 'http://foo') # not changed - self.assertEqual(c.sid, '123') # not changed - self.assertEqual(c.transport(), 'websocket') - self.assertEqual(c.ws, ws) - self.assertEqual( - ws.send_str.mock.call_args_list[0], - (('2probe',),)) # ping - self.assertEqual( - ws.send_str.mock.call_args_list[1], - (('5',),)) # upgrade + assert c not in client.connected_clients # was added by polling + assert c.base_url == 'http://foo' # not changed + assert c.sid == '123' # not changed + assert c.transport() == 'websocket' + assert c.ws == ws + assert ws.send_str.mock.call_args_list[0] == (('2probe',),) # ping + assert ws.send_str.mock.call_args_list[1] == (('5',),) # upgrade def test_receive_unknown_packet(self): c = asyncio_client.AsyncClient() - _run(c._receive_packet(packet.Packet(encoded_packet=b'9'))) + _run(c._receive_packet(packet.Packet(encoded_packet='9'))) # should be ignored def test_receive_noop_packet(self): @@ -677,18 +831,19 @@ class TestAsyncClient(unittest.TestCase): _run(c._receive_packet(packet.Packet(packet.NOOP))) # should be ignored - def test_receive_pong_packet(self): + def test_receive_ping_packet(self): c = asyncio_client.AsyncClient() - c.pong_received = False - _run(c._receive_packet(packet.Packet(packet.PONG))) - self.assertTrue(c.pong_received) + c._send_packet = AsyncMock() + _run(c._receive_packet(packet.Packet(packet.PING))) + assert c._send_packet.mock.call_args_list[0][0][0].encode() == '3' def test_receive_message_packet(self): c = asyncio_client.AsyncClient() c._trigger_event = AsyncMock() _run(c._receive_packet(packet.Packet(packet.MESSAGE, {'foo': 'bar'}))) c._trigger_event.mock.assert_called_once_with( - 'message', {'foo': 'bar'}, run_async=True) + 'message', {'foo': 'bar'}, run_async=True + ) def test_receive_close_packet(self): c = asyncio_client.AsyncClient() @@ -701,16 +856,16 @@ class TestAsyncClient(unittest.TestCase): c.queue = c.create_queue() c.state = 'disconnected' _run(c._send_packet(packet.Packet(packet.NOOP))) - self.assertTrue(c.queue.empty()) + assert c.queue.empty() def test_send_packet(self): c = asyncio_client.AsyncClient() c.queue = c.create_queue() c.state = 'connected' _run(c._send_packet(packet.Packet(packet.NOOP))) - self.assertFalse(c.queue.empty()) + assert not c.queue.empty() pkt = _run(c.queue.get()) - self.assertEqual(pkt.packet_type, packet.NOOP) + assert pkt.packet_type == packet.NOOP def test_trigger_event_function(self): result = [] @@ -722,7 +877,7 @@ class TestAsyncClient(unittest.TestCase): c = asyncio_client.AsyncClient() c.on('message', handler=foo_handler) _run(c._trigger_event('message', 'bar')) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_coroutine(self): result = [] @@ -734,7 +889,7 @@ class TestAsyncClient(unittest.TestCase): c = asyncio_client.AsyncClient() c.on('message', handler=foo_handler) _run(c._trigger_event('message', 'bar')) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_function_error(self): def connect_handler(arg): @@ -746,8 +901,8 @@ class TestAsyncClient(unittest.TestCase): c = asyncio_client.AsyncClient() c.on('connect', handler=connect_handler) c.on('message', handler=foo_handler) - self.assertFalse(_run(c._trigger_event('connect', '123'))) - self.assertIsNone(_run(c._trigger_event('message', 'bar'))) + assert not _run(c._trigger_event('connect', '123')) + assert _run(c._trigger_event('message', 'bar')) is None def test_trigger_event_coroutine_error(self): async def connect_handler(arg): @@ -759,8 +914,8 @@ class TestAsyncClient(unittest.TestCase): c = asyncio_client.AsyncClient() c.on('connect', handler=connect_handler) c.on('message', handler=foo_handler) - self.assertFalse(_run(c._trigger_event('connect', '123'))) - self.assertIsNone(_run(c._trigger_event('message', 'bar'))) + assert not _run(c._trigger_event('connect', '123')) + assert _run(c._trigger_event('message', 'bar')) is None def test_trigger_event_function_async(self): result = [] @@ -773,7 +928,7 @@ class TestAsyncClient(unittest.TestCase): c.on('message', handler=foo_handler) fut = _run(c._trigger_event('message', 'bar', run_async=True)) asyncio.get_event_loop().run_until_complete(fut) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_coroutine_async(self): result = [] @@ -786,7 +941,7 @@ class TestAsyncClient(unittest.TestCase): c.on('message', handler=foo_handler) fut = _run(c._trigger_event('message', 'bar', run_async=True)) asyncio.get_event_loop().run_until_complete(fut) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_function_async_error(self): result = [] @@ -798,10 +953,9 @@ class TestAsyncClient(unittest.TestCase): c = asyncio_client.AsyncClient() c.on('message', handler=foo_handler) fut = _run(c._trigger_event('message', 'bar', run_async=True)) - self.assertRaises( - ZeroDivisionError, asyncio.get_event_loop().run_until_complete, - fut) - self.assertEqual(result, ['bar']) + with pytest.raises(ZeroDivisionError): + asyncio.get_event_loop().run_until_complete(fut) + assert result == ['bar'] def test_trigger_event_coroutine_async_error(self): result = [] @@ -813,10 +967,9 @@ class TestAsyncClient(unittest.TestCase): c = asyncio_client.AsyncClient() c.on('message', handler=foo_handler) fut = _run(c._trigger_event('message', 'bar', run_async=True)) - self.assertRaises( - ZeroDivisionError, asyncio.get_event_loop().run_until_complete, - fut) - self.assertEqual(result, ['bar']) + with pytest.raises(ZeroDivisionError): + asyncio.get_event_loop().run_until_complete(fut) + assert result == ['bar'] def test_trigger_unknown_event(self): c = asyncio_client.AsyncClient() @@ -824,82 +977,11 @@ class TestAsyncClient(unittest.TestCase): _run(c._trigger_event('message', 123, run_async=True)) # should do nothing - def test_ping_loop_disconnected(self): - c = asyncio_client.AsyncClient() - c.state = 'disconnected' - _run(c._ping_loop()) - # should not block - - def test_ping_loop_disconnect(self): - c = asyncio_client.AsyncClient() - c.state = 'connected' - c.ping_interval = 10 - c._send_packet = AsyncMock() - - states = [ - ('disconnecting', True) - ] - - async def fake_wait(): - c.state, c.pong_received = states.pop(0) - - c.ping_loop_event = c.create_event() - c.ping_loop_event.wait = fake_wait - _run(c._ping_loop()) - self.assertEqual( - c._send_packet.mock.call_args_list[0][0][0].encode(), b'2') - - def test_ping_loop_missing_pong(self): - c = asyncio_client.AsyncClient() - c.state = 'connected' - c.ping_interval = 10 - c._send_packet = AsyncMock() - c.queue = mock.MagicMock() - c.queue.put = AsyncMock() - - states = [ - ('connected', False) - ] - - async def fake_wait(): - c.state, c.pong_received = states.pop(0) - - c.ping_loop_event = c.create_event() - c.ping_loop_event.wait = fake_wait - _run(c._ping_loop()) - self.assertEqual(c.state, 'connected') - c.queue.put.mock.assert_called_once_with(None) - - def test_ping_loop_missing_pong_websocket(self): - c = asyncio_client.AsyncClient() - c.state = 'connected' - c.ping_interval = 10 - c._send_packet = AsyncMock() - c.queue = mock.MagicMock() - c.queue.put = AsyncMock() - c.ws = mock.MagicMock() - c.ws.close = AsyncMock() - - states = [ - ('connected', False) - ] - - async def fake_wait(): - c.state, c.pong_received = states.pop(0) - - c.ping_loop_event = c.create_event() - c.ping_loop_event.wait = fake_wait - _run(c._ping_loop()) - self.assertEqual(c.state, 'connected') - c.queue.put.mock.assert_called_once_with(None) - c.ws.close.mock.assert_called_once_with() - def test_read_loop_polling_disconnected(self): c = asyncio_client.AsyncClient() c.state = 'disconnected' c._trigger_event = AsyncMock() c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() _run(c._read_loop_polling()) c._trigger_event.mock.assert_not_called() # should not block @@ -916,14 +998,15 @@ class TestAsyncClient(unittest.TestCase): c._send_request = AsyncMock(return_value=None) c._trigger_event = AsyncMock() c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() _run(c._read_loop_polling()) - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.mock.assert_called_once_with(None) c._send_request.mock.assert_called_once_with( - 'GET', 'http://foo&t=123.456', timeout=30) - c._trigger_event.mock.assert_called_once_with('disconnect', - run_async=False) + 'GET', 'http://foo&t=123.456', timeout=30 + ) + c._trigger_event.mock.assert_called_once_with( + 'disconnect', run_async=False + ) @mock.patch('engineio.client.time.time', return_value=123.456) def test_read_loop_polling_bad_status(self, _time): @@ -937,12 +1020,12 @@ class TestAsyncClient(unittest.TestCase): c._send_request = AsyncMock() c._send_request.mock.return_value.status = 400 c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() _run(c._read_loop_polling()) - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.mock.assert_called_once_with(None) c._send_request.mock.assert_called_once_with( - 'GET', 'http://foo&t=123.456', timeout=30) + 'GET', 'http://foo&t=123.456', timeout=30 + ) @mock.patch('engineio.client.time.time', return_value=123.456) def test_read_loop_polling_bad_packet(self, _time): @@ -955,15 +1038,14 @@ class TestAsyncClient(unittest.TestCase): c.queue.put = AsyncMock() c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 - c._send_request.mock.return_value.read = AsyncMock( - return_value=b'foo') + c._send_request.mock.return_value.read = AsyncMock(return_value=b'foo') c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() _run(c._read_loop_polling()) - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.mock.assert_called_once_with(None) c._send_request.mock.assert_called_once_with( - 'GET', 'http://foo&t=123.456', timeout=65) + 'GET', 'http://foo&t=123.456', timeout=65 + ) def test_read_loop_polling(self): c = asyncio_client.AsyncClient() @@ -975,50 +1057,72 @@ class TestAsyncClient(unittest.TestCase): c.queue.put = AsyncMock() c._send_request = AsyncMock() c._send_request.mock.side_effect = [ - mock.MagicMock(status=200, read=AsyncMock( - return_value=payload.Payload(packets=[ - packet.Packet(packet.PING), - packet.Packet(packet.NOOP)]).encode())), - None + mock.MagicMock( + status=200, + read=AsyncMock( + return_value=payload.Payload( + packets=[ + packet.Packet(packet.PING), + packet.Packet(packet.NOOP), + ] + ).encode().encode('utf-8') + ), + ), + None, ] c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() c._receive_packet = AsyncMock() _run(c._read_loop_polling()) - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.mock.assert_called_once_with(None) - self.assertEqual(c._send_request.mock.call_count, 2) - self.assertEqual(c._receive_packet.mock.call_count, 2) - self.assertEqual( - c._receive_packet.mock.call_args_list[0][0][0].encode(), b'2') - self.assertEqual( - c._receive_packet.mock.call_args_list[1][0][0].encode(), b'6') + assert c._send_request.mock.call_count == 2 + assert c._receive_packet.mock.call_count == 2 + assert c._receive_packet.mock.call_args_list[0][0][0].encode() == '2' + assert c._receive_packet.mock.call_args_list[1][0][0].encode() == '6' def test_read_loop_websocket_disconnected(self): c = asyncio_client.AsyncClient() c.state = 'disconnected' c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() _run(c._read_loop_websocket()) # should not block + def test_read_loop_websocket_timeout(self): + c = asyncio_client.AsyncClient() + c.ping_interval = 1 + c.ping_timeout = 2 + c.base_url = 'ws://foo' + c.state = 'connected' + c.queue = mock.MagicMock() + c.queue.put = AsyncMock() + c.ws = mock.MagicMock() + c.ws.receive = AsyncMock(side_effect=asyncio.TimeoutError()) + c.write_loop_task = AsyncMock()() + _run(c._read_loop_websocket()) + assert c.state == 'disconnected' + c.queue.put.mock.assert_called_once_with(None) + def test_read_loop_websocket_no_response(self): c = asyncio_client.AsyncClient() + c.ping_interval = 1 + c.ping_timeout = 2 c.base_url = 'ws://foo' c.state = 'connected' c.queue = mock.MagicMock() c.queue.put = AsyncMock() c.ws = mock.MagicMock() c.ws.receive = AsyncMock( - side_effect=aiohttp.client_exceptions.ServerDisconnectedError()) + side_effect=aiohttp.client_exceptions.ServerDisconnectedError() + ) c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() _run(c._read_loop_websocket()) - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.mock.assert_called_once_with(None) def test_read_loop_websocket_unexpected_error(self): c = asyncio_client.AsyncClient() + c.ping_interval = 1 + c.ping_timeout = 2 c.base_url = 'ws://foo' c.state = 'connected' c.queue = mock.MagicMock() @@ -1026,28 +1130,30 @@ class TestAsyncClient(unittest.TestCase): c.ws = mock.MagicMock() c.ws.receive = AsyncMock(side_effect=ValueError) c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() _run(c._read_loop_websocket()) - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.mock.assert_called_once_with(None) def test_read_loop_websocket(self): c = asyncio_client.AsyncClient() + c.ping_interval = 1 + c.ping_timeout = 2 c.base_url = 'ws://foo' c.state = 'connected' c.queue = mock.MagicMock() c.queue.put = AsyncMock() c.ws = mock.MagicMock() - c.ws.receive = AsyncMock(side_effect=[ - mock.MagicMock(data=packet.Packet(packet.PING).encode()), - ValueError]) + c.ws.receive = AsyncMock( + side_effect=[ + mock.MagicMock(data=packet.Packet(packet.PING).encode()), + ValueError, + ] + ) c.write_loop_task = AsyncMock()() - c.ping_loop_task = AsyncMock()() c._receive_packet = AsyncMock() _run(c._read_loop_websocket()) - self.assertEqual(c.state, 'disconnected') - self.assertEqual( - c._receive_packet.mock.call_args_list[0][0][0].encode(), b'2') + assert c.state == 'disconnected' + assert c._receive_packet.mock.call_args_list[0][0][0].encode() == '2' c.queue.put.mock.assert_called_once_with(None) def test_write_loop_disconnected(self): @@ -1087,20 +1193,27 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + RuntimeError, + ] + ) c.queue.get_nowait = mock.MagicMock(side_effect=RuntimeError) c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 1) + assert c.queue.task_done.call_count == 1 p = payload.Payload( - packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})]) + packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) c._send_request.mock.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) def test_write_loop_polling_three_packets(self): c = asyncio_client.AsyncClient() @@ -1111,27 +1224,37 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - RuntimeError - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - packet.Packet(packet.PING), - packet.Packet(packet.NOOP), - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + RuntimeError, + ] + ) + c.queue.get_nowait = mock.MagicMock( + side_effect=[ + packet.Packet(packet.PING), + packet.Packet(packet.NOOP), + RuntimeError, + ] + ) c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 3) - p = payload.Payload(packets=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - packet.Packet(packet.PING), - packet.Packet(packet.NOOP), - ]) + assert c.queue.task_done.call_count == 3 + p = payload.Payload( + packets=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + packet.Packet(packet.PING), + packet.Packet(packet.NOOP), + ] + ) c._send_request.mock.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) def test_write_loop_polling_two_packets_done(self): c = asyncio_client.AsyncClient() @@ -1142,26 +1265,33 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - RuntimeError - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - packet.Packet(packet.PING), - None - ]) + c.queue.get = AsyncMock( + side_effect=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + RuntimeError, + ] + ) + c.queue.get_nowait = mock.MagicMock( + side_effect=[packet.Packet(packet.PING), None] + ) c._send_request = AsyncMock() c._send_request.mock.return_value.status = 200 _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 3) - p = payload.Payload(packets=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - packet.Packet(packet.PING), - ]) + assert c.queue.task_done.call_count == 3 + p = payload.Payload( + packets=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + packet.Packet(packet.PING), + ] + ) c._send_request.mock.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) - self.assertEqual(c.state, 'connected') + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) + assert c.state == 'connected' def test_write_loop_polling_bad_connection(self): c = asyncio_client.AsyncClient() @@ -1172,21 +1302,24 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) + c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c._send_request = AsyncMock(return_value=None) _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 1) + assert c.queue.task_done.call_count == 1 p = payload.Payload( - packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})]) + packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) c._send_request.mock.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) - self.assertEqual(c.state, 'connected') + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) + assert c.state == 'connected' def test_write_loop_polling_bad_status(self): c = asyncio_client.AsyncClient() @@ -1197,22 +1330,25 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'polling' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) + c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c._send_request = AsyncMock() c._send_request.mock.return_value.status = 500 _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 1) + assert c.queue.task_done.call_count == 1 p = payload.Payload( - packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})]) + packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) c._send_request.mock.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) - self.assertEqual(c.state, 'disconnected') + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) + assert c.state == 'disconnected' def test_write_loop_websocket_one_packet(self): c = asyncio_client.AsyncClient() @@ -1222,18 +1358,18 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - RuntimeError - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + RuntimeError, + ] + ) + c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c.ws = mock.MagicMock() c.ws.send_str = AsyncMock() _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 1) - self.assertEqual(c.ws.send_str.mock.call_count, 1) + assert c.queue.task_done.call_count == 1 + assert c.ws.send_str.mock.call_count == 1 c.ws.send_str.mock.assert_called_once_with('4{"foo":"bar"}') def test_write_loop_websocket_three_packets(self): @@ -1244,24 +1380,27 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - RuntimeError - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - packet.Packet(packet.PING), - packet.Packet(packet.NOOP), - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + RuntimeError, + ] + ) + c.queue.get_nowait = mock.MagicMock( + side_effect=[ + packet.Packet(packet.PING), + packet.Packet(packet.NOOP), + RuntimeError, + ] + ) c.ws = mock.MagicMock() c.ws.send_str = AsyncMock() _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 3) - self.assertEqual(c.ws.send_str.mock.call_count, 3) - self.assertEqual(c.ws.send_str.mock.call_args_list[0][0][0], - '4{"foo":"bar"}') - self.assertEqual(c.ws.send_str.mock.call_args_list[1][0][0], '2') - self.assertEqual(c.ws.send_str.mock.call_args_list[2][0][0], '6') + assert c.queue.task_done.call_count == 3 + assert c.ws.send_str.mock.call_count == 3 + assert c.ws.send_str.mock.call_args_list[0][0][0] == '4{"foo":"bar"}' + assert c.ws.send_str.mock.call_args_list[1][0][0] == '2' + assert c.ws.send_str.mock.call_args_list[2][0][0] == '6' def test_write_loop_websocket_one_packet_binary(self): c = asyncio_client.AsyncClient() @@ -1271,19 +1410,16 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, b'foo'), - RuntimeError - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[packet.Packet(packet.MESSAGE, b'foo'), RuntimeError] + ) + c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c.ws = mock.MagicMock() c.ws.send_bytes = AsyncMock() _run(c._write_loop()) - self.assertEqual(c.queue.task_done.call_count, 1) - self.assertEqual(c.ws.send_bytes.mock.call_count, 1) - c.ws.send_bytes.mock.assert_called_once_with(b'\x04foo') + assert c.queue.task_done.call_count == 1 + assert c.ws.send_bytes.mock.call_count == 1 + c.ws.send_bytes.mock.assert_called_once_with(b'foo') def test_write_loop_websocket_bad_connection(self): c = asyncio_client.AsyncClient() @@ -1293,15 +1429,30 @@ class TestAsyncClient(unittest.TestCase): c.current_transport = 'websocket' c.queue = mock.MagicMock() c.queue.Empty = RuntimeError - c.queue.get = AsyncMock(side_effect=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - RuntimeError - ]) - c.queue.get_nowait = mock.MagicMock(side_effect=[ - RuntimeError - ]) + c.queue.get = AsyncMock( + side_effect=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + RuntimeError, + ] + ) + c.queue.get_nowait = mock.MagicMock(side_effect=[RuntimeError]) c.ws = mock.MagicMock() c.ws.send_str = AsyncMock( - side_effect=aiohttp.client_exceptions.ServerDisconnectedError()) + side_effect=aiohttp.client_exceptions.ServerDisconnectedError() + ) _run(c._write_loop()) - self.assertEqual(c.state, 'connected') + assert c.state == 'connected' + + @mock.patch('engineio.client.original_signal_handler') + def test_signal_handler(self, original_handler): + clients = [mock.MagicMock(), mock.MagicMock()] + client.connected_clients = clients[:] + client.connected_clients[0].is_asyncio_based.return_value = False + client.connected_clients[1].is_asyncio_based.return_value = True + + async def test(): + asyncio_client.async_signal_handler() + + asyncio.get_event_loop().run_until_complete(test()) + clients[0].disconnect.assert_not_called() + clients[1].disconnect.assert_called_once_with() diff --git a/debian/tests/asyncio/test_asyncio_server.py b/debian/tests/asyncio/test_asyncio_server.py index 54c7254..383ae81 100644 --- a/debian/tests/asyncio/test_asyncio_server.py +++ b/debian/tests/asyncio/test_asyncio_server.py @@ -1,16 +1,14 @@ import asyncio import gzip +import io import json import logging import sys import unittest +from unittest import mock import zlib -import six -if six.PY3: - from unittest import mock -else: - import mock +import pytest from engineio import asyncio_server from engineio.async_drivers import aiohttp as async_aiohttp @@ -39,6 +37,11 @@ def _run(coro): class TestAsyncServer(unittest.TestCase): @staticmethod def get_async_mock(environ={'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''}): + if environ.get('QUERY_STRING'): + if 'EIO=' not in environ['QUERY_STRING']: + environ['QUERY_STRING'] = 'EIO=4&' + environ['QUERY_STRING'] + else: + environ['QUERY_STRING'] = 'EIO=4' a = mock.MagicMock() a._async = { 'asyncio': True, @@ -82,39 +85,36 @@ class TestAsyncServer(unittest.TestCase): def test_is_asyncio_based(self): s = asyncio_server.AsyncServer() - self.assertEqual(s.is_asyncio_based(), True) + assert s.is_asyncio_based() def test_async_modes(self): s = asyncio_server.AsyncServer() - self.assertEqual(s.async_modes(), ['aiohttp', 'sanic', 'tornado', - 'asgi']) + assert s.async_modes() == ['aiohttp', 'sanic', 'tornado', 'asgi'] def test_async_mode_aiohttp(self): s = asyncio_server.AsyncServer(async_mode='aiohttp') - self.assertEqual(s.async_mode, 'aiohttp') - self.assertEqual(s._async['asyncio'], True) - self.assertEqual(s._async['create_route'], async_aiohttp.create_route) - self.assertEqual(s._async['translate_request'], - async_aiohttp.translate_request) - self.assertEqual(s._async['make_response'], - async_aiohttp.make_response) - self.assertEqual(s._async['websocket'].__name__, 'WebSocket') + assert s.async_mode == 'aiohttp' + assert s._async['asyncio'] + assert s._async['create_route'] == async_aiohttp.create_route + assert s._async['translate_request'] == async_aiohttp.translate_request + assert s._async['make_response'] == async_aiohttp.make_response + assert s._async['websocket'].__name__ == 'WebSocket' @mock.patch('importlib.import_module') def test_async_mode_auto_aiohttp(self, import_module): import_module.side_effect = [self.get_async_mock()] s = asyncio_server.AsyncServer() - self.assertEqual(s.async_mode, 'aiohttp') + assert s.async_mode == 'aiohttp' def test_async_modes_wsgi(self): - self.assertRaises(ValueError, asyncio_server.AsyncServer, - async_mode='eventlet') - self.assertRaises(ValueError, asyncio_server.AsyncServer, - async_mode='gevent') - self.assertRaises(ValueError, asyncio_server.AsyncServer, - async_mode='gevent_uwsgi') - self.assertRaises(ValueError, asyncio_server.AsyncServer, - async_mode='threading') + with pytest.raises(ValueError): + asyncio_server.AsyncServer(async_mode='eventlet') + with pytest.raises(ValueError): + asyncio_server.AsyncServer(async_mode='gevent') + with pytest.raises(ValueError): + asyncio_server.AsyncServer(async_mode='gevent_uwsgi') + with pytest.raises(ValueError): + asyncio_server.AsyncServer(async_mode='threading') @mock.patch('importlib.import_module') def test_attach(self, import_module): @@ -138,7 +138,7 @@ class TestAsyncServer(unittest.TestCase): async with s.session('foo') as session: await s.sleep(0) session['username'] = 'bar' - self.assertEqual(await s.get_session('foo'), {'username': 'bar'}) + assert await s.get_session('foo') == {'username': 'bar'} _run(_func()) @@ -146,50 +146,53 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() _run(s.disconnect('foo')) - self.assertEqual(mock_socket.close.mock.call_count, 1) + assert mock_socket.close.mock.call_count == 1 mock_socket.close.mock.assert_called_once_with() - self.assertNotIn('foo', s.sockets) + assert 'foo' not in s.sockets def test_disconnect_all(self): s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_foo = self._get_mock_socket() s.sockets['bar'] = mock_bar = self._get_mock_socket() _run(s.disconnect()) - self.assertEqual(mock_foo.close.mock.call_count, 1) - self.assertEqual(mock_bar.close.mock.call_count, 1) + assert mock_foo.close.mock.call_count == 1 + assert mock_bar.close.mock.call_count == 1 mock_foo.close.mock.assert_called_once_with() mock_bar.close.mock.assert_called_once_with() - self.assertNotIn('foo', s.sockets) - self.assertNotIn('bar', s.sockets) + assert 'foo' not in s.sockets + assert 'bar' not in s.sockets @mock.patch('importlib.import_module') def test_jsonp_not_supported(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'j=abc'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'j=abc'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() response = _run(s.handle_request('request')) - self.assertEqual(response, 'response') + assert response == 'response' a._async['translate_request'].assert_called_once_with('request') - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert a._async['make_response'].call_count == 1 + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') def test_jsonp_index(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'j=233'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'j=233'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() response = _run(s.handle_request('request')) - self.assertEqual(response, 'response') + assert response == 'response' a._async['translate_request'].assert_called_once_with('request') - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') - self.assertTrue(a._async['make_response'].call_args[0][2].startswith( - b'___eio[233]("')) - self.assertTrue(a._async['make_response'].call_args[0][2].endswith( - b'");')) + assert a._async['make_response'].call_count == 1 + assert a._async['make_response'].call_args[0][0] == '200 OK' + assert ( + a._async['make_response'] + .call_args[0][2] + .startswith(b'___eio[233]("') + ) + assert a._async['make_response'].call_args[0][2].endswith(b'");') @mock.patch('importlib.import_module') def test_connect(self, import_module): @@ -197,42 +200,47 @@ class TestAsyncServer(unittest.TestCase): import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(len(s.sockets), 1) - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'application/octet-stream'), - a._async['make_response'].call_args[0][1]) + assert len(s.sockets) == 1 + assert a._async['make_response'].call_count == 1 + assert a._async['make_response'].call_args[0][0] == '200 OK' + assert ('Content-Type', 'text/plain; charset=UTF-8') in a._async[ + 'make_response' + ].call_args[0][1] packets = payload.Payload( - encoded_payload=a._async['make_response'].call_args[0][2]).packets - self.assertEqual(len(packets), 1) - self.assertEqual(packets[0].packet_type, packet.OPEN) - self.assertIn('upgrades', packets[0].data) - self.assertEqual(packets[0].data['upgrades'], ['websocket']) - self.assertIn('sid', packets[0].data) + encoded_payload=a._async['make_response'].call_args[0][2].decode( + 'utf-8')).packets + assert len(packets) == 1 + assert packets[0].packet_type == packet.OPEN + assert 'upgrades' in packets[0].data + assert packets[0].data['upgrades'] == ['websocket'] + assert 'sid' in packets[0].data @mock.patch('importlib.import_module') def test_connect_async_request_response_handlers(self, import_module): a = self.get_async_mock() a._async['translate_request'] = AsyncMock( - return_value=a._async['translate_request'].return_value) + return_value=a._async['translate_request'].return_value + ) a._async['make_response'] = AsyncMock( - return_value=a._async['make_response'].return_value) + return_value=a._async['make_response'].return_value + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(len(s.sockets), 1) - self.assertEqual(a._async['make_response'].mock.call_count, 1) - self.assertEqual(a._async['make_response'].mock.call_args[0][0], - '200 OK') - self.assertIn(('Content-Type', 'application/octet-stream'), - a._async['make_response'].mock.call_args[0][1]) - packets = payload.Payload(encoded_payload=a._async[ - 'make_response'].mock.call_args[0][2]).packets - self.assertEqual(len(packets), 1) - self.assertEqual(packets[0].packet_type, packet.OPEN) - self.assertIn('upgrades', packets[0].data) - self.assertEqual(packets[0].data['upgrades'], ['websocket']) - self.assertIn('sid', packets[0].data) + assert len(s.sockets) == 1 + assert a._async['make_response'].mock.call_count == 1 + assert a._async['make_response'].mock.call_args[0][0] == '200 OK' + assert ('Content-Type', 'text/plain; charset=UTF-8') in a._async[ + 'make_response' + ].mock.call_args[0][1] + packets = payload.Payload( + encoded_payload=a._async['make_response'].mock.call_args[0][ + 2].decode('utf-8')).packets + assert len(packets) == 1 + assert packets[0].packet_type == packet.OPEN + assert 'upgrades' in packets[0].data + assert packets[0].data['upgrades'] == ['websocket'] + assert 'sid' in packets[0].data @mock.patch('importlib.import_module') def test_connect_no_upgrades(self, import_module): @@ -241,84 +249,22 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer(allow_upgrades=False) _run(s.handle_request('request')) packets = payload.Payload( - encoded_payload=a._async['make_response'].call_args[0][2]).packets - self.assertEqual(packets[0].data['upgrades'], []) - - @mock.patch('importlib.import_module') - def test_connect_b64_with_1(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'b64=1'}) - import_module.side_effect = [a] - s = asyncio_server.AsyncServer(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'text/plain; charset=UTF-8'), - a._async['make_response'].call_args[0][1]) - _run(s.send('1', b'\x00\x01\x02', binary=True)) - a._async['translate_request'].return_value = { - 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=1'} - _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][2], - b'6:b4AAEC') + encoded_payload=a._async['make_response'].call_args[0][2].decode( + 'utf-8')).packets + assert packets[0].data['upgrades'] == [] @mock.patch('importlib.import_module') - def test_connect_b64_with_true(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'b64=true'}) + def test_connect_bad_eio_version(self, import_module): + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=1'} + ) import_module.side_effect = [a] - s = asyncio_server.AsyncServer(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'text/plain; charset=UTF-8'), - a._async['make_response'].call_args[0][1]) - _run(s.send('1', b'\x00\x01\x02', binary=True)) - a._async['translate_request'].return_value = { - 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=true'} - _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][2], - b'6:b4AAEC') - - @mock.patch('importlib.import_module') - def test_connect_b64_with_0(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'b64=0'}) - import_module.side_effect = [a] - s = asyncio_server.AsyncServer(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'application/octet-stream'), - a._async['make_response'].call_args[0][1]) - _run(s.send('1', b'\x00\x01\x02', binary=True)) - a._async['translate_request'].return_value = { - 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=0'} - _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][2], - b'\x01\x04\xff\x04\x00\x01\x02') - - @mock.patch('importlib.import_module') - def test_connect_b64_with_false(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'b64=false'}) - import_module.side_effect = [a] - s = asyncio_server.AsyncServer(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'application/octet-stream'), - a._async['make_response'].call_args[0][1]) - _run(s.send('1', b'\x00\x01\x02', binary=True)) - a._async['translate_request'].return_value = { - 'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=false'} + s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][2], - b'\x01\x04\xff\x04\x00\x01\x02') + assert a._async['make_response'].call_count == 1 + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' + assert b'unsupported version' in \ + a._async['make_response'].call_args[0][2] @mock.patch('importlib.import_module') def test_connect_custom_ping_times(self, import_module): @@ -327,9 +273,10 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer(ping_timeout=123, ping_interval=456) _run(s.handle_request('request')) packets = payload.Payload( - encoded_payload=a._async['make_response'].call_args[0][2]).packets - self.assertEqual(packets[0].data['pingTimeout'], 123000) - self.assertEqual(packets[0].data['pingInterval'], 456000) + encoded_payload=a._async['make_response'].call_args[0][2].decode( + 'utf-8')).packets + assert packets[0].data['pingTimeout'] == 123000 + assert packets[0].data['pingInterval'] == 456000 @mock.patch('engineio.asyncio_socket.AsyncSocket') @mock.patch('importlib.import_module') @@ -340,36 +287,69 @@ class TestAsyncServer(unittest.TestCase): AsyncSocket.return_value.poll.side_effect = [exceptions.QueueEmpty] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert a._async['make_response'].call_count == 1 + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('engineio.asyncio_socket.AsyncSocket') @mock.patch('importlib.import_module') def test_connect_transport_websocket(self, import_module, AsyncSocket): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'transport=websocket'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'transport=websocket', + 'HTTP_UPGRADE': 'websocket', + } + ) import_module.side_effect = [a] AsyncSocket.return_value = self._get_mock_socket() s = asyncio_server.AsyncServer() - s._generate_id = mock.MagicMock(return_value='123') + s.generate_id = mock.MagicMock(return_value='123') # force socket to stay open, so that we can check it later AsyncSocket().closed = False _run(s.handle_request('request')) - self.assertEqual( - s.sockets['123'].send.mock.call_args[0][0].packet_type, - packet.OPEN) + assert ( + s.sockets['123'].send.mock.call_args[0][0].packet_type + == packet.OPEN + ) @mock.patch('engineio.asyncio_socket.AsyncSocket') @mock.patch('importlib.import_module') - def test_connect_transport_websocket_closed(self, import_module, - AsyncSocket): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'transport=websocket'}) + def test_http_upgrade_case_insensitive(self, import_module, AsyncSocket): + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'transport=websocket', + 'HTTP_UPGRADE': 'WebSocket', + } + ) import_module.side_effect = [a] AsyncSocket.return_value = self._get_mock_socket() s = asyncio_server.AsyncServer() - s._generate_id = mock.MagicMock(return_value='123') + s.generate_id = mock.MagicMock(return_value='123') + # force socket to stay open, so that we can check it later + AsyncSocket().closed = False + _run(s.handle_request('request')) + assert ( + s.sockets['123'].send.mock.call_args[0][0].packet_type + == packet.OPEN + ) + + @mock.patch('engineio.asyncio_socket.AsyncSocket') + @mock.patch('importlib.import_module') + def test_connect_transport_websocket_closed( + self, import_module, AsyncSocket + ): + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'transport=websocket', + 'HTTP_UPGRADE': 'websocket' + } + ) + import_module.side_effect = [a] + AsyncSocket.return_value = self._get_mock_socket() + s = asyncio_server.AsyncServer() + s.generate_id = mock.MagicMock(return_value='123') # this mock handler just closes the socket, as it would happen on a # real websocket exchange @@ -378,18 +358,29 @@ class TestAsyncServer(unittest.TestCase): AsyncSocket().handle_get_request = mock_handle _run(s.handle_request('request')) - self.assertNotIn('123', s.sockets) # socket should close on its own + assert '123' not in s.sockets # socket should close on its own @mock.patch('importlib.import_module') def test_connect_transport_invalid(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'transport=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_count, 1) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert a._async['make_response'].call_count == 1 + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' + + @mock.patch('importlib.import_module') + def test_connect_transport_websocket_without_upgrade(self, import_module): + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=websocket'} + ) + import_module.side_effect = [a] + s = asyncio_server.AsyncServer() + _run(s.handle_request('request')) + assert a._async['make_response'].call_count == 1 + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') def test_connect_cors_headers(self, import_module): @@ -397,105 +388,111 @@ class TestAsyncServer(unittest.TestCase): import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Credentials', 'true') in headers @mock.patch('importlib.import_module') def test_connect_cors_allowed_origin(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'b'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'b'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins=['a', 'b']) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'b'), headers) + assert ('Access-Control-Allow-Origin', 'b') in headers @mock.patch('importlib.import_module') def test_connect_cors_not_allowed_origin(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'c'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'c'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins=['a', 'b']) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' headers = a._async['make_response'].call_args[0][1] - self.assertNotIn(('Access-Control-Allow-Origin', 'c'), headers) - self.assertNotIn(('Access-Control-Allow-Origin', '*'), headers) + assert ('Access-Control-Allow-Origin', 'c') not in headers + assert ('Access-Control-Allow-Origin', '*') not in headers @mock.patch('importlib.import_module') - def test_connect_cors_not_allowed_origin_async_response(self, - import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'c'}) + def test_connect_cors_not_allowed_origin_async_response( + self, import_module + ): + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'c'} + ) a._async['make_response'] = AsyncMock( - return_value=a._async['make_response'].return_value) + return_value=a._async['make_response'].return_value + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins=['a', 'b']) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].mock.call_args[0][0], - '400 BAD REQUEST') + assert ( + a._async['make_response'].mock.call_args[0][0] == '400 BAD REQUEST' + ) headers = a._async['make_response'].mock.call_args[0][1] - self.assertNotIn(('Access-Control-Allow-Origin', 'c'), headers) - self.assertNotIn(('Access-Control-Allow-Origin', '*'), headers) + assert ('Access-Control-Allow-Origin', 'c') not in headers + assert ('Access-Control-Allow-Origin', '*') not in headers @mock.patch('importlib.import_module') def test_connect_cors_all_origins(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins='*') _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'foo'), headers) - self.assertIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Origin', 'foo') in headers + assert ('Access-Control-Allow-Credentials', 'true') in headers @mock.patch('importlib.import_module') def test_connect_cors_one_origin(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'a'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'a'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins='a') _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'a'), headers) - self.assertIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Origin', 'a') in headers + assert ('Access-Control-Allow-Credentials', 'true') in headers @mock.patch('importlib.import_module') def test_connect_cors_one_origin_not_allowed(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'b'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'HTTP_ORIGIN': 'b'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins='a') _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' headers = a._async['make_response'].call_args[0][1] - self.assertNotIn(('Access-Control-Allow-Origin', 'b'), headers) - self.assertNotIn(('Access-Control-Allow-Origin', '*'), headers) + assert ('Access-Control-Allow-Origin', 'b') not in headers + assert ('Access-Control-Allow-Origin', '*') not in headers @mock.patch('importlib.import_module') def test_connect_cors_headers_default_origin(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'wsgi.url_scheme': 'http', - 'HTTP_HOST': 'foo', - 'HTTP_ORIGIN': 'http://foo'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': '', + 'wsgi.url_scheme': 'http', + 'HTTP_HOST': 'foo', + 'HTTP_ORIGIN': 'http://foo', + } + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'http://foo'), - headers) + assert ('Access-Control-Allow-Origin', 'http://foo') in headers @mock.patch('importlib.import_module') def test_connect_cors_no_credentials(self, import_module): @@ -503,36 +500,41 @@ class TestAsyncServer(unittest.TestCase): import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_credentials=False) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] - self.assertNotIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Credentials', 'true') not in headers @mock.patch('importlib.import_module') def test_connect_cors_options(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'OPTIONS', - 'QUERY_STRING': ''}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'OPTIONS', 'QUERY_STRING': ''} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_credentials=False) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Access-Control-Allow-Methods', - 'OPTIONS, GET, POST'), headers) + assert ( + 'Access-Control-Allow-Methods', + 'OPTIONS, GET, POST', + ) in headers @mock.patch('importlib.import_module') def test_connect_cors_disabled(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'http://foo'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': '', + 'HTTP_ORIGIN': 'http://foo', + } + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins=[]) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: - self.assertFalse(header[0].startswith('Access-Control-')) + assert not header[0].startswith('Access-Control-') @mock.patch('importlib.import_module') def test_connect_cors_default_no_origin(self, import_module): @@ -540,11 +542,10 @@ class TestAsyncServer(unittest.TestCase): import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins=[]) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: - self.assertNotEqual(header[0], 'Access-Control-Allow-Origin') + assert header[0] != 'Access-Control-Allow-Origin' @mock.patch('importlib.import_module') def test_connect_cors_all_no_origin(self, import_module): @@ -552,11 +553,10 @@ class TestAsyncServer(unittest.TestCase): import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins='*') _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: - self.assertNotEqual(header[0], 'Access-Control-Allow-Origin') + assert header[0] != 'Access-Control-Allow-Origin' @mock.patch('importlib.import_module') def test_connect_cors_disabled_no_origin(self, import_module): @@ -564,41 +564,58 @@ class TestAsyncServer(unittest.TestCase): import_module.side_effect = [a] s = asyncio_server.AsyncServer(cors_allowed_origins=[]) _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' headers = a._async['make_response'].call_args[0][1] for header in headers: - self.assertNotEqual(header[0], 'Access-Control-Allow-Origin') + assert header[0] != 'Access-Control-Allow-Origin' @mock.patch('importlib.import_module') def test_connect_event(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = asyncio_server.AsyncServer() - s._generate_id = mock.MagicMock(return_value='123') + s.generate_id = mock.MagicMock(return_value='123') def mock_connect(sid, environ): return True s.on('connect', handler=mock_connect) _run(s.handle_request('request')) - self.assertEqual(len(s.sockets), 1) + assert len(s.sockets) == 1 @mock.patch('importlib.import_module') def test_connect_event_rejects(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = asyncio_server.AsyncServer() - s._generate_id = mock.MagicMock(return_value='123') + s.generate_id = mock.MagicMock(return_value='123') def mock_connect(sid, environ): return False s.on('connect')(mock_connect) _run(s.handle_request('request')) - self.assertEqual(len(s.sockets), 0) - self.assertEqual(a._async['make_response'].call_args[0][0], - '401 UNAUTHORIZED') + assert len(s.sockets) == 0 + assert a._async['make_response'].call_args[0][0] == '401 UNAUTHORIZED' + assert a._async['make_response'].call_args[0][2] == b'"Unauthorized"' + + @mock.patch('importlib.import_module') + def test_connect_event_rejects_with_message(self, import_module): + a = self.get_async_mock() + import_module.side_effect = [a] + s = asyncio_server.AsyncServer() + s.generate_id = mock.MagicMock(return_value='123') + + def mock_connect(sid, environ): + return {'not': 'allowed'} + + s.on('connect')(mock_connect) + _run(s.handle_request('request')) + assert len(s.sockets) == 0 + assert a._async['make_response'].call_args[0][0] == '401 UNAUTHORIZED' + assert ( + a._async['make_response'].call_args[0][2] == b'{"not": "allowed"}' + ) @mock.patch('importlib.import_module') def test_method_not_found(self, import_module): @@ -606,31 +623,32 @@ class TestAsyncServer(unittest.TestCase): import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(len(s.sockets), 0) - self.assertEqual(a._async['make_response'].call_args[0][0], - '405 METHOD NOT FOUND') + assert len(s.sockets) == 0 + assert ( + a._async['make_response'].call_args[0][0] == '405 METHOD NOT FOUND' + ) @mock.patch('importlib.import_module') def test_get_request_with_bad_sid(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(len(s.sockets), 0) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert len(s.sockets) == 0 + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') def test_post_request_with_bad_sid(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'POST', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() _run(s.handle_request('request')) - self.assertEqual(len(s.sockets), 0) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert len(s.sockets) == 0 + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @mock.patch('importlib.import_module') def test_send(self, import_module): @@ -639,10 +657,11 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() _run(s.send('foo', 'hello')) - self.assertEqual(mock_socket.send.mock.call_count, 1) - self.assertEqual(mock_socket.send.mock.call_args[0][0].packet_type, - packet.MESSAGE) - self.assertEqual(mock_socket.send.mock.call_args[0][0].data, 'hello') + assert mock_socket.send.mock.call_count == 1 + assert ( + mock_socket.send.mock.call_args[0][0].packet_type == packet.MESSAGE + ) + assert mock_socket.send.mock.call_args[0][0].data == 'hello' @mock.patch('importlib.import_module') def test_send_unknown_socket(self, import_module): @@ -654,35 +673,41 @@ class TestAsyncServer(unittest.TestCase): @mock.patch('importlib.import_module') def test_get_request(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() - mock_socket.handle_get_request.mock.return_value = \ - [packet.Packet(packet.MESSAGE, data='hello')] + mock_socket.handle_get_request.mock.return_value = [ + packet.Packet(packet.MESSAGE, data='hello') + ] _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' packets = payload.Payload( - encoded_payload=a._async['make_response'].call_args[0][2]).packets - self.assertEqual(len(packets), 1) - self.assertEqual(packets[0].packet_type, packet.MESSAGE) + encoded_payload=a._async['make_response'].call_args[0][2].decode( + 'utf-8') + ).packets + assert len(packets) == 1 + assert packets[0].packet_type == packet.MESSAGE @mock.patch('importlib.import_module') def test_get_request_custom_response(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() mock_socket.handle_get_request.mock.return_value = 'resp' r = _run(s.handle_request('request')) - self.assertEqual(r, 'resp') + assert r == 'resp' @mock.patch('importlib.import_module') def test_get_request_closes_socket(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() @@ -693,13 +718,14 @@ class TestAsyncServer(unittest.TestCase): mock_socket.handle_get_request = mock_get_request r = _run(s.handle_request('request')) - self.assertEqual(r, 'resp') - self.assertNotIn('foo', s.sockets) + assert r == 'resp' + assert 'foo' not in s.sockets @mock.patch('importlib.import_module') def test_get_request_error(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() @@ -709,24 +735,25 @@ class TestAsyncServer(unittest.TestCase): mock_socket.handle_get_request = mock_get_request _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') - self.assertEqual(len(s.sockets), 0) + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' + assert len(s.sockets) == 0 @mock.patch('importlib.import_module') def test_post_request(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'POST', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() s.sockets['foo'] = self._get_mock_socket() _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], '200 OK') + assert a._async['make_response'].call_args[0][0] == '200 OK' @mock.patch('importlib.import_module') def test_post_request_error(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'POST', - 'QUERY_STRING': 'sid=foo'}) + a = self.get_async_mock( + {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer() s.sockets['foo'] = mock_socket = self._get_mock_socket() @@ -736,148 +763,198 @@ class TestAsyncServer(unittest.TestCase): mock_socket.handle_post_request = mock_post_request _run(s.handle_request('request')) - self.assertEqual(a._async['make_response'].call_args[0][0], - '400 BAD REQUEST') + assert a._async['make_response'].call_args[0][0] == '400 BAD REQUEST' @staticmethod def _gzip_decompress(b): - bytesio = six.BytesIO(b) + bytesio = io.BytesIO(b) with gzip.GzipFile(fileobj=bytesio, mode='r') as gz: return gz.read() @mock.patch('importlib.import_module') def test_gzip_compression(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'gzip,deflate'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_ACCEPT_ENCODING': 'gzip,deflate', + } + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() - mock_socket.handle_get_request.mock.return_value = \ - [packet.Packet(packet.MESSAGE, data='hello')] + mock_socket.handle_get_request.mock.return_value = [ + packet.Packet(packet.MESSAGE, data='hello') + ] _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Content-Encoding', 'gzip'), headers) + assert ('Content-Encoding', 'gzip') in headers self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') def test_deflate_compression(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'deflate;q=1,gzip'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_ACCEPT_ENCODING': 'deflate;q=1,gzip', + } + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() - mock_socket.handle_get_request.mock.return_value = \ - [packet.Packet(packet.MESSAGE, data='hello')] + mock_socket.handle_get_request.mock.return_value = [ + packet.Packet(packet.MESSAGE, data='hello') + ] _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Content-Encoding', 'deflate'), headers) + assert ('Content-Encoding', 'deflate') in headers zlib.decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') def test_gzip_compression_threshold(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'gzip'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_ACCEPT_ENCODING': 'gzip', + } + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(compression_threshold=1000) s.sockets['foo'] = mock_socket = self._get_mock_socket() - mock_socket.handle_get_request.mock.return_value = \ - [packet.Packet(packet.MESSAGE, data='hello')] + mock_socket.handle_get_request.mock.return_value = [ + packet.Packet(packet.MESSAGE, data='hello') + ] _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] for header, value in headers: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, - a._async['make_response'].call_args[0][2]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') def test_compression_disabled(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'gzip'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_ACCEPT_ENCODING': 'gzip', + } + ) import_module.side_effect = [a] - s = asyncio_server.AsyncServer(http_compression=False, - compression_threshold=0) + s = asyncio_server.AsyncServer( + http_compression=False, compression_threshold=0 + ) s.sockets['foo'] = mock_socket = self._get_mock_socket() - mock_socket.handle_get_request.mock.return_value = \ - [packet.Packet(packet.MESSAGE, data='hello')] + mock_socket.handle_get_request.mock.return_value = [ + packet.Packet(packet.MESSAGE, data='hello') + ] _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] for header, value in headers: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, - a._async['make_response'].call_args[0][2]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') def test_compression_unknown(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'rar'}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_ACCEPT_ENCODING': 'rar', + } + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() - mock_socket.handle_get_request.mock.return_value = \ - [packet.Packet(packet.MESSAGE, data='hello')] + mock_socket.handle_get_request.mock.return_value = [ + packet.Packet(packet.MESSAGE, data='hello') + ] _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] for header, value in headers: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, - a._async['make_response'].call_args[0][2]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') def test_compression_no_encoding(self, import_module): - a = self.get_async_mock({'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': ''}) + a = self.get_async_mock( + { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_ACCEPT_ENCODING': '', + } + ) import_module.side_effect = [a] s = asyncio_server.AsyncServer(compression_threshold=0) s.sockets['foo'] = mock_socket = self._get_mock_socket() - mock_socket.handle_get_request.mock.return_value = \ - [packet.Packet(packet.MESSAGE, data='hello')] + mock_socket.handle_get_request.mock.return_value = [ + packet.Packet(packet.MESSAGE, data='hello') + ] _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] for header, value in headers: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, - a._async['make_response'].call_args[0][2]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(a._async['make_response'].call_args[0][2]) @mock.patch('importlib.import_module') def test_cookie(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = asyncio_server.AsyncServer(cookie='sid') - s._generate_id = mock.MagicMock(return_value='123') + s.generate_id = mock.MagicMock(return_value='123') + _run(s.handle_request('request')) + headers = a._async['make_response'].call_args[0][1] + assert ('Set-Cookie', 'sid=123; path=/; SameSite=Lax') in headers + + @mock.patch('importlib.import_module') + def test_cookie_dict(self, import_module): + def get_path(): + return '/a' + + a = self.get_async_mock() + import_module.side_effect = [a] + s = asyncio_server.AsyncServer(cookie={ + 'name': 'test', + 'path': get_path, + 'SameSite': 'None', + 'Secure': True, + 'HttpOnly': True + }) + s.generate_id = mock.MagicMock(return_value='123') _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] - self.assertIn(('Set-Cookie', 'sid=123'), headers) + assert ('Set-Cookie', 'test=123; path=/a; SameSite=None; Secure; ' + 'HttpOnly') in headers @mock.patch('importlib.import_module') def test_no_cookie(self, import_module): a = self.get_async_mock() import_module.side_effect = [a] s = asyncio_server.AsyncServer(cookie=None) - s._generate_id = mock.MagicMock(return_value='123') + s.generate_id = mock.MagicMock(return_value='123') _run(s.handle_request('request')) headers = a._async['make_response'].call_args[0][1] for header, value in headers: - self.assertNotEqual(header, 'Set-Cookie') + assert header != 'Set-Cookie' def test_logger(self): s = asyncio_server.AsyncServer(logger=False) - self.assertEqual(s.logger.getEffectiveLevel(), logging.ERROR) + assert s.logger.getEffectiveLevel() == logging.ERROR s.logger.setLevel(logging.NOTSET) s = asyncio_server.AsyncServer(logger=True) - self.assertEqual(s.logger.getEffectiveLevel(), logging.INFO) + assert s.logger.getEffectiveLevel() == logging.INFO s.logger.setLevel(logging.WARNING) s = asyncio_server.AsyncServer(logger=True) - self.assertEqual(s.logger.getEffectiveLevel(), logging.WARNING) + assert s.logger.getEffectiveLevel() == logging.WARNING s.logger.setLevel(logging.NOTSET) my_logger = logging.Logger('foo') s = asyncio_server.AsyncServer(logger=my_logger) - self.assertEqual(s.logger, my_logger) + assert s.logger == my_logger def test_custom_json(self): # Warning: this test cannot run in parallel with other tests, as it @@ -894,9 +971,9 @@ class TestAsyncServer(unittest.TestCase): asyncio_server.AsyncServer(json=CustomJSON) pkt = packet.Packet(packet.MESSAGE, data={'foo': 'bar'}) - self.assertEqual(pkt.encode(), b'4*** encoded ***') + assert pkt.encode() == '4*** encoded ***' pkt2 = packet.Packet(encoded_packet=pkt.encode()) - self.assertEqual(pkt2.data, '+++ decoded +++') + assert pkt2.data == '+++ decoded +++' # restore the default JSON module packet.Packet.json = json @@ -909,9 +986,10 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.start_background_task(foo, 'bar') - pending = asyncio.Task.all_tasks() + pending = asyncio.all_tasks(loop=asyncio.get_event_loop()) \ + if hasattr(asyncio, 'all_tasks') else asyncio.Task.all_tasks() asyncio.get_event_loop().run_until_complete(asyncio.wait(pending)) - self.assertEqual(r, ['bar']) + assert r == ['bar'] def test_sleep(self): s = asyncio_server.AsyncServer() @@ -927,7 +1005,7 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.on('message', handler=foo_handler) _run(s._trigger_event('message', 'bar')) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_coroutine(self): result = [] @@ -939,7 +1017,7 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.on('message', handler=foo_handler) _run(s._trigger_event('message', 'bar')) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_function_error(self): def connect_handler(arg): @@ -951,8 +1029,8 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.on('connect', handler=connect_handler) s.on('message', handler=foo_handler) - self.assertFalse(_run(s._trigger_event('connect', '123'))) - self.assertIsNone(_run(s._trigger_event('message', 'bar'))) + assert not _run(s._trigger_event('connect', '123')) + assert _run(s._trigger_event('message', 'bar')) is None def test_trigger_event_coroutine_error(self): async def connect_handler(arg): @@ -964,8 +1042,8 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.on('connect', handler=connect_handler) s.on('message', handler=foo_handler) - self.assertFalse(_run(s._trigger_event('connect', '123'))) - self.assertIsNone(_run(s._trigger_event('message', 'bar'))) + assert not _run(s._trigger_event('connect', '123')) + assert _run(s._trigger_event('message', 'bar')) is None def test_trigger_event_function_async(self): result = [] @@ -978,7 +1056,7 @@ class TestAsyncServer(unittest.TestCase): s.on('message', handler=foo_handler) fut = _run(s._trigger_event('message', 'bar', run_async=True)) asyncio.get_event_loop().run_until_complete(fut) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_coroutine_async(self): result = [] @@ -991,7 +1069,7 @@ class TestAsyncServer(unittest.TestCase): s.on('message', handler=foo_handler) fut = _run(s._trigger_event('message', 'bar', run_async=True)) asyncio.get_event_loop().run_until_complete(fut) - self.assertEqual(result, ['ok', 'bar']) + assert result == ['ok', 'bar'] def test_trigger_event_function_async_error(self): result = [] @@ -1003,10 +1081,9 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.on('message', handler=foo_handler) fut = _run(s._trigger_event('message', 'bar', run_async=True)) - self.assertRaises( - ZeroDivisionError, asyncio.get_event_loop().run_until_complete, - fut) - self.assertEqual(result, ['bar']) + with pytest.raises(ZeroDivisionError): + asyncio.get_event_loop().run_until_complete(fut) + assert result == ['bar'] def test_trigger_event_coroutine_async_error(self): result = [] @@ -1018,23 +1095,23 @@ class TestAsyncServer(unittest.TestCase): s = asyncio_server.AsyncServer() s.on('message', handler=foo_handler) fut = _run(s._trigger_event('message', 'bar', run_async=True)) - self.assertRaises( - ZeroDivisionError, asyncio.get_event_loop().run_until_complete, - fut) - self.assertEqual(result, ['bar']) + with pytest.raises(ZeroDivisionError): + asyncio.get_event_loop().run_until_complete(fut) + assert result == ['bar'] def test_create_queue(self): s = asyncio_server.AsyncServer() q = s.create_queue() empty = s.get_queue_empty_exception() - self.assertRaises(empty, q.get_nowait) + with pytest.raises(empty): + q.get_nowait() def test_create_event(self): s = asyncio_server.AsyncServer() e = s.create_event() - self.assertFalse(e.is_set()) + assert not e.is_set() e.set() - self.assertTrue(e.is_set()) + assert e.is_set() @mock.patch('importlib.import_module') def test_service_task_started(self, import_module): diff --git a/debian/tests/asyncio/test_asyncio_socket.py b/debian/tests/asyncio/test_asyncio_socket.py index 91013ab..147c1b7 100644 --- a/debian/tests/asyncio/test_asyncio_socket.py +++ b/debian/tests/asyncio/test_asyncio_socket.py @@ -2,12 +2,9 @@ import asyncio import sys import time import unittest +from unittest import mock -import six -if six.PY3: - from unittest import mock -else: - import mock +import pytest from engineio import asyncio_socket from engineio import exceptions @@ -45,39 +42,46 @@ class TestSocket(unittest.TestCase): mock_server.ping_interval = 0.2 mock_server.ping_interval_grace_period = 0.001 mock_server.async_handlers = False - mock_server._async = {'asyncio': True, - 'create_route': mock.MagicMock(), - 'translate_request': mock.MagicMock(), - 'make_response': mock.MagicMock(), - 'websocket': 'w'} + mock_server._async = { + 'asyncio': True, + 'create_route': mock.MagicMock(), + 'translate_request': mock.MagicMock(), + 'make_response': mock.MagicMock(), + 'websocket': 'w', + } mock_server._async['translate_request'].return_value = 'request' mock_server._async['make_response'].return_value = 'response' mock_server._trigger_event = AsyncMock() + def bg_task(target, *args, **kwargs): + return asyncio.ensure_future(target(*args, **kwargs)) + def create_queue(*args, **kwargs): queue = asyncio.Queue(*args, **kwargs) queue.Empty = asyncio.QueueEmpty return queue + mock_server.start_background_task = bg_task mock_server.create_queue = create_queue return mock_server def test_create(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') - self.assertEqual(s.server, mock_server) - self.assertEqual(s.sid, 'sid') - self.assertFalse(s.upgraded) - self.assertFalse(s.closed) - self.assertTrue(hasattr(s.queue, 'get')) - self.assertTrue(hasattr(s.queue, 'put')) - self.assertTrue(hasattr(s.queue, 'task_done')) - self.assertTrue(hasattr(s.queue, 'join')) + assert s.server == mock_server + assert s.sid == 'sid' + assert not s.upgraded + assert not s.closed + assert hasattr(s.queue, 'get') + assert hasattr(s.queue, 'put') + assert hasattr(s.queue, 'task_done') + assert hasattr(s.queue, 'join') def test_empty_poll(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') - self.assertRaises(exceptions.QueueEmpty, _run, s.poll()) + with pytest.raises(exceptions.QueueEmpty): + _run(s.poll()) def test_poll(self): mock_server = self._get_mock_server() @@ -86,28 +90,66 @@ class TestSocket(unittest.TestCase): pkt2 = packet.Packet(packet.MESSAGE, data='bye') _run(s.send(pkt1)) _run(s.send(pkt2)) - self.assertEqual(_run(s.poll()), [pkt1, pkt2]) + assert _run(s.poll()) == [pkt1, pkt2] def test_poll_none(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') _run(s.queue.put(None)) - self.assertEqual(_run(s.poll()), []) + assert _run(s.poll()) == [] - def test_ping_pong(self): + def test_poll_none_after_packet(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') - _run(s.receive(packet.Packet(packet.PING, data='abc'))) - r = _run(s.poll()) - self.assertEqual(len(r), 1) - self.assertTrue(r[0].encode(), b'3abc') + pkt = packet.Packet(packet.MESSAGE, data='hello') + _run(s.send(pkt)) + _run(s.queue.put(None)) + assert _run(s.poll()) == [pkt] + assert _run(s.poll()) == [] + + def test_schedule_ping(self): + mock_server = self._get_mock_server() + mock_server.ping_interval = 0.01 + s = asyncio_socket.AsyncSocket(mock_server, 'sid') + s.send = AsyncMock() + + async def schedule_ping(): + s.schedule_ping() + await asyncio.sleep(0.05) + + _run(schedule_ping()) + assert s.last_ping is not None + assert s.send.mock.call_args_list[0][0][0].encode() == '2' + + def test_schedule_ping_closed_socket(self): + mock_server = self._get_mock_server() + mock_server.ping_interval = 0.01 + s = asyncio_socket.AsyncSocket(mock_server, 'sid') + s.send = AsyncMock() + s.closed = True + + async def schedule_ping(): + s.schedule_ping() + await asyncio.sleep(0.05) + + _run(schedule_ping()) + assert s.last_ping is None + s.send.mock.assert_not_called() + + def test_pong(self): + mock_server = self._get_mock_server() + s = asyncio_socket.AsyncSocket(mock_server, 'sid') + s.schedule_ping = mock.MagicMock() + _run(s.receive(packet.Packet(packet.PONG, data='abc'))) + s.schedule_ping.assert_called_once_with() def test_message_sync_handler(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') _run(s.receive(packet.Packet(packet.MESSAGE, data='foo'))) mock_server._trigger_event.mock.assert_called_once_with( - 'message', 'sid', 'foo', run_async=False) + 'message', 'sid', 'foo', run_async=False + ) def test_message_async_handler(self): mock_server = self._get_mock_server() @@ -115,13 +157,14 @@ class TestSocket(unittest.TestCase): mock_server.async_handlers = True _run(s.receive(packet.Packet(packet.MESSAGE, data='foo'))) mock_server._trigger_event.mock.assert_called_once_with( - 'message', 'sid', 'foo', run_async=True) + 'message', 'sid', 'foo', run_async=True + ) def test_invalid_packet(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') - self.assertRaises(exceptions.UnknownPacketError, _run, - s.receive(packet.Packet(packet.OPEN))) + with pytest.raises(exceptions.UnknownPacketError): + _run(s.receive(packet.Packet(packet.OPEN))) def test_timeout(self): mock_server = self._get_mock_server() @@ -142,50 +185,59 @@ class TestSocket(unittest.TestCase): _run(s.send(pkt2)) environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} packets = _run(s.handle_get_request(environ)) - self.assertEqual(packets, [pkt1, pkt2]) + assert packets == [pkt1, pkt2] def test_polling_read_error(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'foo') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} - self.assertRaises(exceptions.QueueEmpty, _run, - s.handle_get_request(environ)) + with pytest.raises(exceptions.QueueEmpty): + _run(s.handle_get_request(environ)) def test_polling_write(self): mock_server = self._get_mock_server() mock_server.max_http_buffer_size = 1000 pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') - p = payload.Payload(packets=[pkt1, pkt2]).encode() + p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') s = asyncio_socket.AsyncSocket(mock_server, 'foo') s.receive = AsyncMock() - environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', - 'CONTENT_LENGTH': len(p), - 'wsgi.input': self._get_read_mock_coro(p)} + environ = { + 'REQUEST_METHOD': 'POST', + 'QUERY_STRING': 'sid=foo', + 'CONTENT_LENGTH': len(p), + 'wsgi.input': self._get_read_mock_coro(p), + } _run(s.handle_post_request(environ)) - self.assertEqual(s.receive.mock.call_count, 2) + assert s.receive.mock.call_count == 2 def test_polling_write_too_large(self): mock_server = self._get_mock_server() pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') - p = payload.Payload(packets=[pkt1, pkt2]).encode() + p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') mock_server.max_http_buffer_size = len(p) - 1 s = asyncio_socket.AsyncSocket(mock_server, 'foo') s.receive = AsyncMock() - environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', - 'CONTENT_LENGTH': len(p), - 'wsgi.input': self._get_read_mock_coro(p)} - self.assertRaises(exceptions.ContentTooLongError, _run, - s.handle_post_request(environ)) + environ = { + 'REQUEST_METHOD': 'POST', + 'QUERY_STRING': 'sid=foo', + 'CONTENT_LENGTH': len(p), + 'wsgi.input': self._get_read_mock_coro(p), + } + with pytest.raises(exceptions.ContentTooLongError): + _run(s.handle_post_request(environ)) def test_upgrade_handshake(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'foo') s._upgrade_websocket = AsyncMock() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_CONNECTION': 'Foo,Upgrade,Bar', - 'HTTP_UPGRADE': 'websocket'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_CONNECTION': 'Foo,Upgrade,Bar', + 'HTTP_UPGRADE': 'websocket', + } _run(s.handle_get_request(environ)) s._upgrade_websocket.mock.assert_called_once_with(environ) @@ -199,7 +251,8 @@ class TestSocket(unittest.TestCase): environ = "foo" _run(s._upgrade_websocket(environ)) mock_server._async['websocket'].assert_called_once_with( - s._websocket_handler) + s._websocket_handler + ) mock_ws.mock.assert_called_once_with(environ) def test_upgrade_twice(self): @@ -209,7 +262,8 @@ class TestSocket(unittest.TestCase): s.connected = True s.upgraded = True environ = "foo" - self.assertRaises(IOError, _run, s._upgrade_websocket(environ)) + with pytest.raises(IOError): + _run(s._upgrade_websocket(environ)) def test_upgrade_packet(self): mock_server = self._get_mock_server() @@ -217,8 +271,8 @@ class TestSocket(unittest.TestCase): s.connected = True _run(s.receive(packet.Packet(packet.UPGRADE))) r = _run(s.poll()) - self.assertEqual(len(r), 1) - self.assertEqual(r[0].encode(), packet.Packet(packet.NOOP).encode()) + assert len(r) == 1 + assert r[0].encode() == packet.Packet(packet.NOOP).encode() def test_upgrade_no_probe(self): mock_server = self._get_mock_server() @@ -226,10 +280,9 @@ class TestSocket(unittest.TestCase): s.connected = True ws = mock.MagicMock() ws.wait = AsyncMock() - ws.wait.mock.return_value = packet.Packet(packet.NOOP).encode( - always_bytes=False) + ws.wait.mock.return_value = packet.Packet(packet.NOOP).encode() _run(s._websocket_handler(ws)) - self.assertFalse(s.upgraded) + assert not s.upgraded def test_upgrade_no_upgrade_packet(self): mock_server = self._get_mock_server() @@ -239,16 +292,17 @@ class TestSocket(unittest.TestCase): ws = mock.MagicMock() ws.send = AsyncMock() ws.wait = AsyncMock() - probe = six.text_type('probe') + probe = 'probe' ws.wait.mock.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.NOOP).encode(always_bytes=False)] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.NOOP).encode(), + ] _run(s._websocket_handler(ws)) - ws.send.mock.assert_called_once_with(packet.Packet( - packet.PONG, data=probe).encode(always_bytes=False)) - self.assertEqual(_run(s.queue.get()).packet_type, packet.NOOP) - self.assertFalse(s.upgraded) + ws.send.mock.assert_called_once_with( + packet.Packet(packet.PONG, data=probe).encode() + ) + assert _run(s.queue.get()).packet_type == packet.NOOP + assert not s.upgraded def test_upgrade_not_supported(self): mock_server = self._get_mock_server() @@ -272,24 +326,28 @@ class TestSocket(unittest.TestCase): s = asyncio_socket.AsyncSocket(mock_server, 'sid') s.connected = False s.queue.join = AsyncMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - s.poll = AsyncMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], None]) + foo = 'foo' + bar = 'bar' + s.poll = AsyncMock( + side_effect=[[packet.Packet(packet.MESSAGE, data=bar)], None] + ) ws = mock.MagicMock() ws.send = AsyncMock() ws.wait = AsyncMock() ws.wait.mock.side_effect = [ - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - None] + packet.Packet(packet.MESSAGE, data=foo).encode(), + None, + ] _run(s._websocket_handler(ws)) - self.assertTrue(s.connected) - self.assertTrue(s.upgraded) - self.assertEqual(mock_server._trigger_event.mock.call_count, 2) - mock_server._trigger_event.mock.assert_has_calls([ - mock.call('message', 'sid', 'foo', run_async=False), - mock.call('disconnect', 'sid')]) + assert s.connected + assert s.upgraded + assert mock_server._trigger_event.mock.call_count == 2 + mock_server._trigger_event.mock.assert_has_calls( + [ + mock.call('message', 'sid', 'foo', run_async=False), + mock.call('disconnect', 'sid'), + ] + ) ws.send.mock.assert_called_with('4bar') def test_websocket_upgrade_read_write(self): @@ -297,27 +355,33 @@ class TestSocket(unittest.TestCase): s = asyncio_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = AsyncMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - probe = six.text_type('probe') - s.poll = AsyncMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty]) + foo = 'foo' + bar = 'bar' + probe = 'probe' + s.poll = AsyncMock( + side_effect=[ + [packet.Packet(packet.MESSAGE, data=bar)], + exceptions.QueueEmpty, + ] + ) ws = mock.MagicMock() ws.send = AsyncMock() ws.wait = AsyncMock() ws.wait.mock.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.UPGRADE).encode(always_bytes=False), - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - None] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.UPGRADE).encode(), + packet.Packet(packet.MESSAGE, data=foo).encode(), + None, + ] _run(s._websocket_handler(ws)) - self.assertTrue(s.upgraded) - self.assertEqual(mock_server._trigger_event.mock.call_count, 2) - mock_server._trigger_event.mock.assert_has_calls([ - mock.call('message', 'sid', 'foo', run_async=False), - mock.call('disconnect', 'sid')]) + assert s.upgraded + assert mock_server._trigger_event.mock.call_count == 2 + mock_server._trigger_event.mock.assert_has_calls( + [ + mock.call('message', 'sid', 'foo', run_async=False), + mock.call('disconnect', 'sid'), + ] + ) ws.send.mock.assert_called_with('4bar') def test_websocket_upgrade_with_payload(self): @@ -325,104 +389,123 @@ class TestSocket(unittest.TestCase): s = asyncio_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = AsyncMock(return_value=None) - probe = six.text_type('probe') + probe = 'probe' ws = mock.MagicMock() ws.send = AsyncMock() ws.wait = AsyncMock() ws.wait.mock.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.UPGRADE, data=b'2').encode( - always_bytes=False)] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.UPGRADE, data='2').encode(), + ] _run(s._websocket_handler(ws)) - self.assertTrue(s.upgraded) + assert s.upgraded def test_websocket_upgrade_with_backlog(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') s.connected = True s.queue.join = AsyncMock(return_value=None) - probe = six.text_type('probe') - foo = six.text_type('foo') + probe = 'probe' + foo = 'foo' ws = mock.MagicMock() ws.send = AsyncMock() ws.wait = AsyncMock() ws.wait.mock.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.UPGRADE, data=b'2').encode( - always_bytes=False)] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.UPGRADE, data='2').encode(), + ] s.upgrading = True _run(s.send(packet.Packet(packet.MESSAGE, data=foo))) + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=sid'} + packets = _run(s.handle_get_request(environ)) + assert len(packets) == 1 + assert packets[0].encode() == '6' + packets = _run(s.poll()) + assert len(packets) == 1 + assert packets[0].encode() == '4foo' + _run(s._websocket_handler(ws)) - self.assertTrue(s.upgraded) - self.assertFalse(s.upgrading) - self.assertEqual(s.packet_backlog, []) - ws.send.mock.assert_called_with('4foo') + assert s.upgraded + assert not s.upgrading + packets = _run(s.handle_get_request(environ)) + assert len(packets) == 1 + assert packets[0].encode() == '6' def test_websocket_read_write_wait_fail(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') s.connected = False s.queue.join = AsyncMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - s.poll = AsyncMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], - [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty]) + foo = 'foo' + bar = 'bar' + s.poll = AsyncMock( + side_effect=[ + [packet.Packet(packet.MESSAGE, data=bar)], + [packet.Packet(packet.MESSAGE, data=bar)], + exceptions.QueueEmpty, + ] + ) ws = mock.MagicMock() ws.send = AsyncMock() ws.wait = AsyncMock() ws.wait.mock.side_effect = [ - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - RuntimeError] + packet.Packet(packet.MESSAGE, data=foo).encode(), + RuntimeError, + ] ws.send.mock.side_effect = [None, RuntimeError] _run(s._websocket_handler(ws)) - self.assertEqual(s.closed, True) + assert s.closed def test_websocket_ignore_invalid_packet(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') s.connected = False s.queue.join = AsyncMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - s.poll = AsyncMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty]) + foo = 'foo' + bar = 'bar' + s.poll = AsyncMock( + side_effect=[ + [packet.Packet(packet.MESSAGE, data=bar)], + exceptions.QueueEmpty, + ] + ) ws = mock.MagicMock() ws.send = AsyncMock() ws.wait = AsyncMock() ws.wait.mock.side_effect = [ - packet.Packet(packet.OPEN).encode(always_bytes=False), - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - None] + packet.Packet(packet.OPEN).encode(), + packet.Packet(packet.MESSAGE, data=foo).encode(), + None, + ] _run(s._websocket_handler(ws)) - self.assertTrue(s.connected) - self.assertEqual(mock_server._trigger_event.mock.call_count, 2) - mock_server._trigger_event.mock.assert_has_calls([ - mock.call('message', 'sid', foo, run_async=False), - mock.call('disconnect', 'sid')]) + assert s.connected + assert mock_server._trigger_event.mock.call_count == 2 + mock_server._trigger_event.mock.assert_has_calls( + [ + mock.call('message', 'sid', foo, run_async=False), + mock.call('disconnect', 'sid'), + ] + ) ws.send.mock.assert_called_with('4bar') def test_send_after_close(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') _run(s.close(wait=False)) - self.assertRaises(exceptions.SocketIsClosedError, _run, - s.send(packet.Packet(packet.NOOP))) + with pytest.raises(exceptions.SocketIsClosedError): + _run(s.send(packet.Packet(packet.NOOP))) def test_close_after_close(self): mock_server = self._get_mock_server() s = asyncio_socket.AsyncSocket(mock_server, 'sid') _run(s.close(wait=False)) - self.assertTrue(s.closed) - self.assertEqual(mock_server._trigger_event.mock.call_count, 1) - mock_server._trigger_event.mock.assert_called_once_with('disconnect', - 'sid') + assert s.closed + assert mock_server._trigger_event.mock.call_count == 1 + mock_server._trigger_event.mock.assert_called_once_with( + 'disconnect', 'sid' + ) _run(s.close()) - self.assertEqual(mock_server._trigger_event.mock.call_count, 1) + assert mock_server._trigger_event.mock.call_count == 1 def test_close_and_wait(self): mock_server = self._get_mock_server() @@ -440,4 +523,4 @@ class TestSocket(unittest.TestCase): s.queue.put = AsyncMock() s.queue.join = AsyncMock() _run(s.close(wait=False)) - self.assertEqual(s.queue.join.mock.call_count, 0) + assert s.queue.join.mock.call_count == 0 diff --git a/debian/tests/common/test_async_eventlet.py b/debian/tests/common/test_async_eventlet.py index c516094..2509677 100644 --- a/debian/tests/common/test_async_eventlet.py +++ b/debian/tests/common/test_async_eventlet.py @@ -1,13 +1,9 @@ import logging import unittest - -import six -if six.PY3: - from unittest import mock -else: - import mock +from unittest import mock from engineio.async_drivers import eventlet as async_eventlet +import pytest class TestAsyncEventlet(unittest.TestCase): @@ -18,13 +14,16 @@ class TestAsyncEventlet(unittest.TestCase): wsgi = async_eventlet.WebSocketWSGI(None) environ = {'foo': 'bar'} start_response = 'bar' - self.assertRaises(RuntimeError, wsgi, environ, start_response) + with pytest.raises(RuntimeError): + wsgi(environ, start_response) - @mock.patch('engineio.async_drivers.eventlet._WebSocketWSGI.__call__', - return_value='data') + @mock.patch( + 'engineio.async_drivers.eventlet._WebSocketWSGI.__call__', + return_value='data', + ) def test_wsgi_call(self, _WebSocketWSGI): _WebSocketWSGI.__call__ = lambda e, s: 'data' environ = {'eventlet.input': mock.MagicMock()} start_response = 'bar' wsgi = async_eventlet.WebSocketWSGI(None) - self.assertEqual(wsgi(environ, start_response), 'data') + assert wsgi(environ, start_response) == 'data' diff --git a/debian/tests/common/test_client.py b/debian/tests/common/test_client.py index 63a579a..532c078 100644 --- a/debian/tests/common/test_client.py +++ b/debian/tests/common/test_client.py @@ -3,12 +3,9 @@ import logging import ssl import time import unittest +from unittest import mock -import six -if six.PY3: - from unittest import mock -else: - import mock +import pytest import websocket from engineio import client @@ -16,51 +13,57 @@ from engineio import exceptions from engineio import packet from engineio import payload -if six.PY2: - ConnectionError = OSError - class TestClient(unittest.TestCase): def test_is_asyncio_based(self): c = client.Client() - self.assertEqual(c.is_asyncio_based(), False) + assert not c.is_asyncio_based() def test_create(self): c = client.Client() - self.assertEqual(c.handlers, {}) - for attr in ['base_url', 'transports', 'sid', 'upgrades', - 'ping_interval', 'ping_timeout', 'http', 'ws', - 'read_loop_task', 'write_loop_task', 'queue']: - self.assertIsNone(getattr(c, attr), attr + ' is not None') - self.assertTrue(c.pong_received) - self.assertEqual(c.state, 'disconnected') + assert c.handlers == {} + for attr in [ + 'base_url', + 'transports', + 'sid', + 'upgrades', + 'ping_interval', + 'ping_timeout', + 'http', + 'ws', + 'read_loop_task', + 'write_loop_task', + 'queue', + ]: + assert getattr(c, attr) is None, attr + ' is not None' + assert c.state == 'disconnected' def test_custon_json(self): client.Client() - self.assertEqual(packet.Packet.json, json) + assert packet.Packet.json == json client.Client(json='foo') - self.assertEqual(packet.Packet.json, 'foo') + assert packet.Packet.json == 'foo' packet.Packet.json = json def test_logger(self): c = client.Client(logger=False) - self.assertEqual(c.logger.getEffectiveLevel(), logging.ERROR) + assert c.logger.getEffectiveLevel() == logging.ERROR c.logger.setLevel(logging.NOTSET) c = client.Client(logger=True) - self.assertEqual(c.logger.getEffectiveLevel(), logging.INFO) + assert c.logger.getEffectiveLevel() == logging.INFO c.logger.setLevel(logging.WARNING) c = client.Client(logger=True) - self.assertEqual(c.logger.getEffectiveLevel(), logging.WARNING) + assert c.logger.getEffectiveLevel() == logging.WARNING c.logger.setLevel(logging.NOTSET) my_logger = logging.Logger('foo') c = client.Client(logger=my_logger) - self.assertEqual(c.logger, my_logger) + assert c.logger == my_logger def test_custon_timeout(self): c = client.Client() - self.assertEqual(c.request_timeout, 5) + assert c.request_timeout == 5 c = client.Client(request_timeout=27) - self.assertEqual(c.request_timeout, 27) + assert c.request_timeout == 27 def test_on_event(self): c = client.Client() @@ -68,82 +71,89 @@ class TestClient(unittest.TestCase): @c.on('connect') def foo(): pass + c.on('disconnect', foo) - self.assertEqual(c.handlers['connect'], foo) - self.assertEqual(c.handlers['disconnect'], foo) + assert c.handlers['connect'] == foo + assert c.handlers['disconnect'] == foo def test_on_event_invalid(self): c = client.Client() - self.assertRaises(ValueError, c.on, 'invalid') + with pytest.raises(ValueError): + c.on('invalid') def test_already_connected(self): c = client.Client() c.state = 'connected' - self.assertRaises(ValueError, c.connect, 'http://foo') + with pytest.raises(ValueError): + c.connect('http://foo') def test_invalid_transports(self): c = client.Client() - self.assertRaises(ValueError, c.connect, 'http://foo', - transports=['foo', 'bar']) + with pytest.raises(ValueError): + c.connect('http://foo', transports=['foo', 'bar']) def test_some_invalid_transports(self): c = client.Client() c._connect_websocket = mock.MagicMock() c.connect('http://foo', transports=['foo', 'websocket', 'bar']) - self.assertEqual(c.transports, ['websocket']) + assert c.transports == ['websocket'] def test_connect_polling(self): c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') - self.assertEqual(c.connect('http://foo'), 'foo') + assert c.connect('http://foo') == 'foo' c._connect_polling.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') - self.assertEqual(c.connect('http://foo', transports=['polling']), - 'foo') + assert c.connect('http://foo', transports=['polling']) == 'foo' c._connect_polling.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') - self.assertEqual(c.connect('http://foo', - transports=['polling', 'websocket']), - 'foo') + assert ( + c.connect('http://foo', transports=['polling', 'websocket']) + == 'foo' + ) c._connect_polling.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) def test_connect_websocket(self): c = client.Client() c._connect_websocket = mock.MagicMock(return_value='foo') - self.assertEqual(c.connect('http://foo', transports=['websocket']), - 'foo') + assert c.connect('http://foo', transports=['websocket']) == 'foo' c._connect_websocket.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) c = client.Client() c._connect_websocket = mock.MagicMock(return_value='foo') - self.assertEqual(c.connect('http://foo', transports='websocket'), - 'foo') + assert c.connect('http://foo', transports='websocket') == 'foo' c._connect_websocket.assert_called_once_with( - 'http://foo', {}, 'engine.io') + 'http://foo', {}, 'engine.io' + ) def test_connect_query_string(self): c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') - self.assertEqual(c.connect('http://foo?bar=baz'), 'foo') + assert c.connect('http://foo?bar=baz') == 'foo' c._connect_polling.assert_called_once_with( - 'http://foo?bar=baz', {}, 'engine.io') + 'http://foo?bar=baz', {}, 'engine.io' + ) def test_connect_custom_headers(self): c = client.Client() c._connect_polling = mock.MagicMock(return_value='foo') - self.assertEqual(c.connect('http://foo', headers={'Foo': 'Bar'}), - 'foo') + assert c.connect('http://foo', headers={'Foo': 'Bar'}) == 'foo' c._connect_polling.assert_called_once_with( - 'http://foo', {'Foo': 'Bar'}, 'engine.io') + 'http://foo', {'Foo': 'Bar'}, 'engine.io' + ) def test_wait(self): c = client.Client() @@ -165,26 +175,25 @@ class TestClient(unittest.TestCase): c._send_packet = fake_send_packet c.send('foo') - c.send('foo', binary=False) - c.send(b'foo', binary=True) - self.assertEqual(saved_packets[0].packet_type, packet.MESSAGE) - self.assertEqual(saved_packets[0].data, 'foo') - self.assertEqual(saved_packets[0].binary, - False if six.PY3 else True) - self.assertEqual(saved_packets[1].packet_type, packet.MESSAGE) - self.assertEqual(saved_packets[1].data, 'foo') - self.assertEqual(saved_packets[1].binary, False) - self.assertEqual(saved_packets[2].packet_type, packet.MESSAGE) - self.assertEqual(saved_packets[2].data, b'foo') - self.assertEqual(saved_packets[2].binary, True) + c.send('foo') + c.send(b'foo') + assert saved_packets[0].packet_type == packet.MESSAGE + assert saved_packets[0].data == 'foo' + assert not saved_packets[0].binary + assert saved_packets[1].packet_type == packet.MESSAGE + assert saved_packets[1].data == 'foo' + assert not saved_packets[1].binary + assert saved_packets[2].packet_type == packet.MESSAGE + assert saved_packets[2].data == b'foo' + assert saved_packets[2].binary def test_disconnect_not_connected(self): c = client.Client() c.state = 'foo' c.sid = 'bar' c.disconnect() - self.assertEqual(c.state, 'disconnected') - self.assertIsNone(c.sid) + assert c.state == 'disconnected' + assert c.sid is None def test_disconnect_polling(self): c = client.Client() @@ -198,9 +207,8 @@ class TestClient(unittest.TestCase): c.disconnect() c.read_loop_task.join.assert_called_once_with() c.ws.mock.assert_not_called() - self.assertNotIn(c, client.connected_clients) - c._trigger_event.assert_called_once_with('disconnect', - run_async=False) + assert c not in client.connected_clients + c._trigger_event.assert_called_once_with('disconnect', run_async=False) def test_disconnect_websocket(self): c = client.Client() @@ -214,9 +222,8 @@ class TestClient(unittest.TestCase): c.disconnect() c.read_loop_task.join.assert_called_once_with() c.ws.close.assert_called_once_with() - self.assertNotIn(c, client.connected_clients) - c._trigger_event.assert_called_once_with('disconnect', - run_async=False) + assert c not in client.connected_clients + c._trigger_event.assert_called_once_with('disconnect', run_async=False) def test_disconnect_polling_abort(self): c = client.Client() @@ -230,7 +237,7 @@ class TestClient(unittest.TestCase): c.queue.join.assert_not_called() c.read_loop_task.join.assert_not_called() c.ws.mock.assert_not_called() - self.assertNotIn(c, client.connected_clients) + assert c not in client.connected_clients def test_disconnect_websocket_abort(self): c = client.Client() @@ -244,12 +251,12 @@ class TestClient(unittest.TestCase): c.queue.join.assert_not_called() c.read_loop_task.join.assert_not_called() c.ws.mock.assert_not_called() - self.assertNotIn(c, client.connected_clients) + assert c not in client.connected_clients def test_current_transport(self): c = client.Client() c.current_transport = 'foo' - self.assertEqual(c.transport(), 'foo') + assert c.transport() == 'foo' def test_background_tasks(self): flag = {} @@ -260,73 +267,115 @@ class TestClient(unittest.TestCase): c = client.Client() task = c.start_background_task(bg_task) task.join() - self.assertIn('task', flag) - self.assertTrue(flag['task']) + assert 'task' in flag + assert flag['task'] def test_sleep(self): c = client.Client() t = time.time() c.sleep(0.1) - self.assertTrue(time.time() - t > 0.1) + assert time.time() - t > 0.1 def test_create_queue(self): c = client.Client() q = c.create_queue() - self.assertRaises(q.Empty, q.get, timeout=0.01) + with pytest.raises(q.Empty): + q.get(timeout=0.01) def test_create_event(self): c = client.Client() e = c.create_event() - self.assertFalse(e.is_set()) + assert not e.is_set() e.set() - self.assertTrue(e.is_set()) + assert e.is_set() @mock.patch('engineio.client.time.time', return_value=123.456) @mock.patch('engineio.client.Client._send_request', return_value=None) def test_polling_connection_failed(self, _send_request, _time): c = client.Client() - self.assertRaises(exceptions.ConnectionError, c.connect, 'http://foo', - headers={'Foo': 'Bar'}) + with pytest.raises(exceptions.ConnectionError): + c.connect('http://foo', headers={'Foo': 'Bar'}) _send_request.assert_called_once_with( - 'GET', 'http://foo/engine.io/?transport=polling&EIO=3&t=123.456', - headers={'Foo': 'Bar'}, timeout=5) + 'GET', + 'http://foo/engine.io/?transport=polling&EIO=4&t=123.456', + headers={'Foo': 'Bar'}, + timeout=5, + ) @mock.patch('engineio.client.Client._send_request') def test_polling_connection_404(self, _send_request): _send_request.return_value.status_code = 404 + _send_request.return_value.json.return_value = {'foo': 'bar'} c = client.Client() - self.assertRaises(exceptions.ConnectionError, c.connect, 'http://foo') + try: + c.connect('http://foo') + except exceptions.ConnectionError as exc: + assert len(exc.args) == 2 + assert ( + exc.args[0] == 'Unexpected status code 404 in server response' + ) + assert exc.args[1] == {'foo': 'bar'} + + @mock.patch('engineio.client.Client._send_request') + def test_polling_connection_404_no_json(self, _send_request): + _send_request.return_value.status_code = 404 + _send_request.return_value.json.side_effect = json.JSONDecodeError( + 'error', '<html></html>', 0) + c = client.Client() + try: + c.connect('http://foo') + except exceptions.ConnectionError as exc: + assert len(exc.args) == 2 + assert ( + exc.args[0] == 'Unexpected status code 404 in server response' + ) + assert exc.args[1] is None @mock.patch('engineio.client.Client._send_request') def test_polling_connection_invalid_packet(self, _send_request): _send_request.return_value.status_code = 200 _send_request.return_value.content = b'foo' c = client.Client() - self.assertRaises(exceptions.ConnectionError, c.connect, 'http://foo') + with pytest.raises(exceptions.ConnectionError): + c.connect('http://foo') @mock.patch('engineio.client.Client._send_request') def test_polling_connection_no_open_packet(self, _send_request): _send_request.return_value.status_code = 200 - _send_request.return_value.content = payload.Payload(packets=[ - packet.Packet(packet.CLOSE, { - 'sid': '123', 'upgrades': [], 'pingInterval': 10, - 'pingTimeout': 20 - }) - ]).encode() - c = client.Client() - self.assertRaises(exceptions.ConnectionError, c.connect, 'http://foo') + _send_request.return_value.content = payload.Payload( + packets=[ + packet.Packet( + packet.CLOSE, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 10, + 'pingTimeout': 20, + }, + ) + ] + ).encode().encode('utf-8') + c = client.Client() + with pytest.raises(exceptions.ConnectionError): + c.connect('http://foo') @mock.patch('engineio.client.Client._send_request') def test_polling_connection_successful(self, _send_request): _send_request.return_value.status_code = 200 - _send_request.return_value.content = payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }) - ]).encode() - c = client.Client() - c._ping_loop = mock.MagicMock() + _send_request.return_value.content = payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') + c = client.Client() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() @@ -335,32 +384,38 @@ class TestClient(unittest.TestCase): c.connect('http://foo') time.sleep(0.1) - c._ping_loop.assert_called_once_with() c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'http://foo/engine.io/?transport=polling&EIO=3&sid=123') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'polling') + assert c in client.connected_clients + assert ( + c.base_url + == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' + ) + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'polling' @mock.patch('engineio.client.Client._send_request') def test_polling_https_noverify_connection_successful(self, _send_request): _send_request.return_value.status_code = 200 - _send_request.return_value.content = payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }) - ]).encode() + _send_request.return_value.content = payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') c = client.Client(ssl_verify=False) - c._ping_loop = mock.MagicMock() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() @@ -369,33 +424,39 @@ class TestClient(unittest.TestCase): c.connect('https://foo') time.sleep(0.1) - c._ping_loop.assert_called_once_with() c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'https://foo/engine.io/?transport=polling&EIO=3&sid=123') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'polling') + assert c in client.connected_clients + assert ( + c.base_url + == 'https://foo/engine.io/?transport=polling&EIO=4&sid=123' + ) + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'polling' @mock.patch('engineio.client.Client._send_request') def test_polling_connection_with_more_packets(self, _send_request): _send_request.return_value.status_code = 200 - _send_request.return_value.content = payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }), - packet.Packet(packet.NOOP) - ]).encode() - c = client.Client() - c._ping_loop = mock.MagicMock() + _send_request.return_value.content = payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ), + packet.Packet(packet.NOOP), + ] + ).encode().encode('utf-8') + c = client.Client() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() @@ -404,50 +465,66 @@ class TestClient(unittest.TestCase): c.on('connect', on_connect) c.connect('http://foo') time.sleep(0.1) - self.assertEqual(c._receive_packet.call_count, 1) - self.assertEqual( - c._receive_packet.call_args_list[0][0][0].packet_type, - packet.NOOP) + assert c._receive_packet.call_count == 1 + assert ( + c._receive_packet.call_args_list[0][0][0].packet_type + == packet.NOOP + ) @mock.patch('engineio.client.Client._send_request') def test_polling_connection_upgraded(self, _send_request): _send_request.return_value.status_code = 200 - _send_request.return_value.content = payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': ['websocket'], 'pingInterval': 1000, - 'pingTimeout': 2000 - }) - ]).encode() + _send_request.return_value.content = payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': ['websocket'], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') c = client.Client() c._connect_websocket = mock.MagicMock(return_value=True) on_connect = mock.MagicMock() c.on('connect', on_connect) c.connect('http://foo') - c._connect_websocket.assert_called_once_with('http://foo', {}, - 'engine.io') + c._connect_websocket.assert_called_once_with( + 'http://foo', {}, 'engine.io' + ) on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'http://foo/engine.io/?transport=polling&EIO=3&sid=123') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, ['websocket']) + assert c in client.connected_clients + assert ( + c.base_url + == 'http://foo/engine.io/?transport=polling&EIO=4&sid=123' + ) + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == ['websocket'] @mock.patch('engineio.client.Client._send_request') def test_polling_connection_not_upgraded(self, _send_request): _send_request.return_value.status_code = 200 - _send_request.return_value.content = payload.Payload(packets=[ - packet.Packet(packet.OPEN, { - 'sid': '123', 'upgrades': ['websocket'], 'pingInterval': 1000, - 'pingTimeout': 2000 - }) - ]).encode() + _send_request.return_value.content = payload.Payload( + packets=[ + packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': ['websocket'], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ) + ] + ).encode().encode('utf-8') c = client.Client() c._connect_websocket = mock.MagicMock(return_value=False) - c._ping_loop = mock.MagicMock() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() @@ -456,54 +533,92 @@ class TestClient(unittest.TestCase): c.connect('http://foo') time.sleep(0.1) - c._connect_websocket.assert_called_once_with('http://foo', {}, - 'engine.io') - c._ping_loop.assert_called_once_with() + c._connect_websocket.assert_called_once_with( + 'http://foo', {}, 'engine.io' + ) c._read_loop_polling.assert_called_once_with() c._read_loop_websocket.assert_not_called() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) + assert c in client.connected_clients @mock.patch('engineio.client.time.time', return_value=123.456) - @mock.patch('engineio.client.websocket.create_connection', - side_effect=[ConnectionError]) + @mock.patch( + 'engineio.client.websocket.create_connection', + side_effect=[ConnectionError], + ) def test_websocket_connection_failed(self, create_connection, _time): c = client.Client() - self.assertRaises(exceptions.ConnectionError, c.connect, 'http://foo', - transports=['websocket'], headers={'Foo': 'Bar'}) + with pytest.raises(exceptions.ConnectionError): + c.connect( + 'http://foo', transports=['websocket'], headers={'Foo': 'Bar'} + ) create_connection.assert_called_once_with( - 'ws://foo/engine.io/?transport=websocket&EIO=3&t=123.456', - header={'Foo': 'Bar'}, cookie=None) + 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', + header={'Foo': 'Bar'}, + cookie=None, + enable_multithread=True, + ) @mock.patch('engineio.client.time.time', return_value=123.456) - @mock.patch('engineio.client.websocket.create_connection', - side_effect=[ConnectionError]) + @mock.patch( + 'engineio.client.websocket.create_connection', + side_effect=[websocket.WebSocketException], + ) + def test_websocket_connection_failed_with_websocket_error( + self, create_connection, _time + ): + c = client.Client() + with pytest.raises(exceptions.ConnectionError): + c.connect( + 'http://foo', transports=['websocket'], headers={'Foo': 'Bar'} + ) + create_connection.assert_called_once_with( + 'ws://foo/engine.io/?transport=websocket&EIO=4&t=123.456', + header={'Foo': 'Bar'}, + cookie=None, + enable_multithread=True, + ) + + @mock.patch('engineio.client.time.time', return_value=123.456) + @mock.patch( + 'engineio.client.websocket.create_connection', + side_effect=[ConnectionError], + ) def test_websocket_upgrade_failed(self, create_connection, _time): c = client.Client() + c.ping_interval = 1 + c.ping_timeout = 2 c.sid = '123' - self.assertFalse(c.connect('http://foo', transports=['websocket'])) + assert not c.connect('http://foo', transports=['websocket']) create_connection.assert_called_once_with( - 'ws://foo/engine.io/?transport=websocket&EIO=3&sid=123&t=123.456', - header={}, cookie=None) + 'ws://foo/engine.io/?transport=websocket&EIO=4&sid=123&t=123.456', + header={}, + cookie=None, + enable_multithread=True, + ) @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_no_open_packet(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( - packet.CLOSE).encode() + packet.CLOSE + ).encode() c = client.Client() - self.assertRaises(exceptions.ConnectionError, c.connect, 'http://foo', - transports=['websocket']) + with pytest.raises(exceptions.ConnectionError): + c.connect('http://foo', transports=['websocket']) @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_successful(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( - packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() c = client.Client() - c._ping_loop = mock.MagicMock() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() @@ -512,35 +627,39 @@ class TestClient(unittest.TestCase): c.connect('ws://foo', transports=['websocket']) time.sleep(0.1) - c._ping_loop.assert_called_once_with() c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'ws://foo/engine.io/?transport=websocket&EIO=3') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'websocket') - self.assertEqual(c.ws, create_connection.return_value) - self.assertEqual(len(create_connection.call_args_list), 1) - self.assertEqual(create_connection.call_args[1], - {'header': {}, 'cookie': None}) + assert c in client.connected_clients + assert c.base_url == 'ws://foo/engine.io/?transport=websocket&EIO=4' + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'websocket' + assert c.ws == create_connection.return_value + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'header': {}, + 'cookie': None, + 'enable_multithread': True, + } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_https_noverify_connection_successful( - self, create_connection): + self, create_connection + ): create_connection.return_value.recv.return_value = packet.Packet( - packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() c = client.Client(ssl_verify=False) - c._ping_loop = mock.MagicMock() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() @@ -549,41 +668,47 @@ class TestClient(unittest.TestCase): c.connect('wss://foo', transports=['websocket']) time.sleep(0.1) - c._ping_loop.assert_called_once_with() c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_called_once_with() - self.assertIn(c, client.connected_clients) - self.assertEqual( - c.base_url, - 'wss://foo/engine.io/?transport=websocket&EIO=3') - self.assertEqual(c.sid, '123') - self.assertEqual(c.ping_interval, 1) - self.assertEqual(c.ping_timeout, 2) - self.assertEqual(c.upgrades, []) - self.assertEqual(c.transport(), 'websocket') - self.assertEqual(c.ws, create_connection.return_value) - self.assertEqual(len(create_connection.call_args_list), 1) - self.assertEqual(create_connection.call_args[1], - {'header': {}, 'cookie': None, - 'sslopt': {'cert_reqs': ssl.CERT_NONE}}) + assert c in client.connected_clients + assert c.base_url == 'wss://foo/engine.io/?transport=websocket&EIO=4' + assert c.sid == '123' + assert c.ping_interval == 1 + assert c.ping_timeout == 2 + assert c.upgrades == [] + assert c.transport() == 'websocket' + assert c.ws == create_connection.return_value + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'header': {}, + 'cookie': None, + 'enable_multithread': True, + 'sslopt': {'cert_reqs': ssl.CERT_NONE}, + } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_cookies(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( - packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() - c = client.Client() - c.http = mock.MagicMock() - c.http.cookies = [mock.MagicMock(), mock.MagicMock()] - c.http.cookies[0].name = 'key' - c.http.cookies[0].value = 'value' - c.http.cookies[1].name = 'key2' - c.http.cookies[1].value = 'value2' - c._ping_loop = mock.MagicMock() + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [mock.MagicMock(), mock.MagicMock()] + http.cookies[0].name = 'key' + http.cookies[0].value = 'value' + http.cookies[1].name = 'key2' + http.cookies[1].value = 'value2' + http.auth = None + http.proxies = None + http.cert = None + c = client.Client(http_session=http) c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() @@ -592,73 +717,349 @@ class TestClient(unittest.TestCase): c.connect('ws://foo', transports=['websocket']) time.sleep(0.1) - self.assertEqual(len(create_connection.call_args_list), 1) - self.assertEqual(create_connection.call_args[1], - {'header': {}, 'cookie': 'key=value; key2=value2'}) + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'header': {}, + 'cookie': 'key=value; key2=value2', + 'enable_multithread': True, + } + + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_with_cookie_header(self, create_connection): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = None + http.proxies = None + http.cert = None + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect( + 'ws://foo', + headers={'Foo': 'bar', 'Cookie': 'key=value'}, + transports=['websocket'], + ) + time.sleep(0.1) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'header': {'Foo': 'bar'}, + 'cookie': 'key=value', + 'enable_multithread': True, + } + + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_with_cookies_and_headers( + self, create_connection + ): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [mock.MagicMock(), mock.MagicMock()] + http.cookies[0].name = 'key' + http.cookies[0].value = 'value' + http.cookies[1].name = 'key2' + http.cookies[1].value = 'value2' + http.auth = None + http.proxies = None + http.cert = None + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect( + 'ws://foo', + headers={'Cookie': 'key3=value3'}, + transports=['websocket'], + ) + time.sleep(0.1) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'header': {}, + 'enable_multithread': True, + 'cookie': 'key=value; key2=value2; key3=value3', + } + + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_with_auth(self, create_connection): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = ('foo', 'bar') + http.proxies = None + http.cert = None + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect('ws://foo', transports=['websocket']) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'header': {'Authorization': 'Basic Zm9vOmJhcg=='}, + 'cookie': '', + 'enable_multithread': True, + } + + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_with_cert(self, create_connection): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = None + http.proxies = None + http.cert = 'foo.crt' + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect('ws://foo', transports=['websocket']) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'sslopt': {'certfile': 'foo.crt'}, + 'header': {}, + 'cookie': '', + 'enable_multithread': True, + } + + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_with_cert_and_key(self, create_connection): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = None + http.proxies = None + http.cert = ('foo.crt', 'key.pem') + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect('ws://foo', transports=['websocket']) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'sslopt': {'certfile': 'foo.crt', 'keyfile': 'key.pem'}, + 'header': {}, + 'cookie': '', + 'enable_multithread': True, + } + + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_with_proxies(self, create_connection): + all_urls = [ + 'ws://foo', + 'ws://foo', + 'ws://foo', + 'ws://foo', + 'ws://foo', + 'wss://foo', + 'wss://foo', + ] + all_proxies = [ + {'http': 'foo.com:1234'}, + {'https': 'foo.com:1234'}, + {'http': 'foo.com:1234', 'ws': 'bar.com:4321'}, + {}, + {'http': 'user:pass@foo.com:1234'}, + {'https': 'foo.com:1234'}, + {'https': 'foo.com:1234', 'wss': 'bar.com:4321'}, + ] + all_results = [ + ('foo.com', 1234, None), + None, + ('bar.com', 4321, None), + None, + ('foo.com', 1234, ('user', 'pass')), + ('foo.com', 1234, None), + ('bar.com', 4321, None), + ] + for url, proxies, results in zip(all_urls, all_proxies, all_results): + create_connection.reset_mock() + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = None + http.proxies = proxies + http.cert = None + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect(url, transports=['websocket']) + + assert len(create_connection.call_args_list) == 1 + expected_results = { + 'header': {}, + 'cookie': '', + 'enable_multithread': True, + } + if results: + expected_results.update({ + 'http_proxy_host': results[0], + 'http_proxy_port': results[1], + 'http_proxy_auth': results[2]}) + assert create_connection.call_args[1] == expected_results + + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_without_verify(self, create_connection): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = None + http.proxies = None + http.cert = None + http.verify = False + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect('ws://foo', transports=['websocket']) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'sslopt': {"cert_reqs": ssl.CERT_NONE}, + 'header': {}, + 'cookie': '', + 'enable_multithread': True, + } @mock.patch('engineio.client.websocket.create_connection') def test_websocket_upgrade_no_pong(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( - packet.OPEN, { - 'sid': '123', 'upgrades': [], 'pingInterval': 1000, - 'pingTimeout': 2000 - }).encode() + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() c = client.Client() c.sid = '123' c.current_transport = 'polling' - c._ping_loop = mock.MagicMock() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) - self.assertFalse(c.connect('ws://foo', transports=['websocket'])) + assert not c.connect('ws://foo', transports=['websocket']) - c._ping_loop.assert_not_called() c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_not_called() c._write_loop.assert_not_called() on_connect.assert_not_called() - self.assertEqual(c.transport(), 'polling') + assert c.transport() == 'polling' @mock.patch('engineio.client.websocket.create_connection') def test_websocket_upgrade_successful(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet( - packet.PONG, six.text_type('probe')).encode() + packet.PONG, 'probe').encode() c = client.Client() + c.ping_interval = 1 + c.ping_timeout = 2 c.sid = '123' c.base_url = 'http://foo' c.current_transport = 'polling' - c._ping_loop = mock.MagicMock() c._read_loop_polling = mock.MagicMock() c._read_loop_websocket = mock.MagicMock() c._write_loop = mock.MagicMock() on_connect = mock.MagicMock() c.on('connect', on_connect) - self.assertTrue(c.connect('ws://foo', transports=['websocket'])) + assert c.connect('ws://foo', transports=['websocket']) time.sleep(0.1) - c._ping_loop.assert_called_once_with() c._read_loop_polling.assert_not_called() c._read_loop_websocket.assert_called_once_with() c._write_loop.assert_called_once_with() on_connect.assert_not_called() # was called by polling - self.assertNotIn(c, client.connected_clients) # was added by polling - self.assertEqual(c.base_url, 'http://foo') # not changed - self.assertEqual(c.sid, '123') # not changed - self.assertEqual(c.transport(), 'websocket') - self.assertEqual(c.ws, create_connection.return_value) - self.assertEqual( - create_connection.return_value.send.call_args_list[0], - ((packet.Packet(packet.PING, six.text_type('probe')).encode(),),) + assert c not in client.connected_clients # was added by polling + assert c.base_url == 'http://foo' # not changed + assert c.sid == '123' # not changed + assert c.transport() == 'websocket' + assert c.ws == create_connection.return_value + assert create_connection.return_value.send.call_args_list[0] == ( + (packet.Packet(packet.PING, 'probe').encode(),), ) # ping - self.assertEqual( - create_connection.return_value.send.call_args_list[1], - ((packet.Packet(packet.UPGRADE).encode(),),)) # upgrade + assert create_connection.return_value.send.call_args_list[1] == ( + (packet.Packet(packet.UPGRADE).encode(),), + ) # upgrade def test_receive_unknown_packet(self): c = client.Client() - c._receive_packet(packet.Packet(encoded_packet=b'9')) + c._receive_packet(packet.Packet(encoded_packet='9')) # should be ignored def test_receive_noop_packet(self): @@ -666,18 +1067,19 @@ class TestClient(unittest.TestCase): c._receive_packet(packet.Packet(packet.NOOP)) # should be ignored - def test_receive_pong_packet(self): + def test_receive_ping_packet(self): c = client.Client() - c.pong_received = False - c._receive_packet(packet.Packet(packet.PONG)) - self.assertTrue(c.pong_received) + c._send_packet = mock.MagicMock() + c._receive_packet(packet.Packet(packet.PING)) + assert c._send_packet.call_args_list[0][0][0].encode() == '3' # PONG def test_receive_message_packet(self): c = client.Client() c._trigger_event = mock.MagicMock() c._receive_packet(packet.Packet(packet.MESSAGE, {'foo': 'bar'})) - c._trigger_event.assert_called_once_with('message', {'foo': 'bar'}, - run_async=True) + c._trigger_event.assert_called_once_with( + 'message', {'foo': 'bar'}, run_async=True + ) def test_receive_close_packet(self): c = client.Client() @@ -690,16 +1092,16 @@ class TestClient(unittest.TestCase): c.queue = c.create_queue() c.state = 'disconnected' c._send_packet(packet.Packet(packet.NOOP)) - self.assertTrue(c.queue.empty()) + assert c.queue.empty() def test_send_packet(self): c = client.Client() c.queue = c.create_queue() c.state = 'connected' c._send_packet(packet.Packet(packet.NOOP)) - self.assertFalse(c.queue.empty()) + assert not c.queue.empty() pkt = c.queue.get() - self.assertEqual(pkt.packet_type, packet.NOOP) + assert pkt.packet_type == packet.NOOP def test_trigger_event(self): c = client.Client() @@ -715,12 +1117,12 @@ class TestClient(unittest.TestCase): return 'bar' r = c._trigger_event('connect', run_async=False) - self.assertEqual(r, 'foo') + assert r == 'foo' r = c._trigger_event('message', 123, run_async=True) r.join() - self.assertEqual(f['bar'], 123) + assert f['bar'] == 123 r = c._trigger_event('message', 321) - self.assertEqual(r, 'bar') + assert r == 'bar' def test_trigger_unknown_event(self): c = client.Client() @@ -740,116 +1142,52 @@ class TestClient(unittest.TestCase): return 1 / 0 r = c._trigger_event('connect', run_async=False) - self.assertEqual(r, None) + assert r is None r = c._trigger_event('message', 123, run_async=False) - self.assertEqual(r, None) + assert r is None def test_engineio_url(self): c = client.Client() - self.assertEqual( - c._get_engineio_url('http://foo', 'bar', 'polling'), - 'http://foo/bar/?transport=polling&EIO=3') - self.assertEqual( - c._get_engineio_url('http://foo', 'bar', 'websocket'), - 'ws://foo/bar/?transport=websocket&EIO=3') - self.assertEqual( - c._get_engineio_url('ws://foo', 'bar', 'polling'), - 'http://foo/bar/?transport=polling&EIO=3') - self.assertEqual( - c._get_engineio_url('ws://foo', 'bar', 'websocket'), - 'ws://foo/bar/?transport=websocket&EIO=3') - self.assertEqual( - c._get_engineio_url('https://foo', 'bar', 'polling'), - 'https://foo/bar/?transport=polling&EIO=3') - self.assertEqual( - c._get_engineio_url('https://foo', 'bar', 'websocket'), - 'wss://foo/bar/?transport=websocket&EIO=3') - self.assertEqual( - c._get_engineio_url('http://foo?baz=1', 'bar', 'polling'), - 'http://foo/bar/?baz=1&transport=polling&EIO=3') - self.assertEqual( - c._get_engineio_url('http://foo#baz', 'bar', 'polling'), - 'http://foo/bar/?transport=polling&EIO=3') - - def test_ping_loop_disconnected(self): - c = client.Client() - c.state = 'disconnected' - c._ping_loop() - # should not block - - def test_ping_loop_disconnect(self): - c = client.Client() - c.state = 'connected' - c.ping_interval = 10 - c._send_packet = mock.MagicMock() - - states = [ - ('disconnecting', True) - ] - - def fake_wait(timeout): - self.assertEqual(timeout, 10) - c.state, c.pong_received = states.pop(0) - - c.ping_loop_event = c.create_event() - c.ping_loop_event.wait = fake_wait - c._ping_loop() - self.assertEqual( - c._send_packet.call_args_list[0][0][0].encode(), b'2') - - def test_ping_loop_missing_pong(self): - c = client.Client() - c.state = 'connected' - c.ping_interval = 10 - c._send_packet = mock.MagicMock() - c.queue = mock.MagicMock() - - states = [ - ('connected', False) - ] - - def fake_wait(timeout): - self.assertEqual(timeout, 10) - c.state, c.pong_received = states.pop(0) - - c.ping_loop_event = c.create_event() - c.ping_loop_event.wait = fake_wait - c._ping_loop() - self.assertEqual(c.state, 'connected') - c.queue.put.assert_called_once_with(None) - - def test_ping_loop_missing_pong_websocket(self): - c = client.Client() - c.state = 'connected' - c.ping_interval = 10 - c._send_packet = mock.MagicMock() - c.queue = mock.MagicMock() - c.ws = mock.MagicMock() - - states = [ - ('connected', False) - ] - - def fake_wait(timeout): - self.assertEqual(timeout, 10) - c.state, c.pong_received = states.pop(0) - - c.ping_loop_event = c.create_event() - c.ping_loop_event.wait = fake_wait - c._ping_loop() - self.assertEqual(c.state, 'connected') - c.queue.put.assert_called_once_with(None) - c.ws.shutdown.assert_called_once_with() + assert ( + c._get_engineio_url('http://foo', 'bar', 'polling') + == 'http://foo/bar/?transport=polling&EIO=4' + ) + assert ( + c._get_engineio_url('http://foo', 'bar', 'websocket') + == 'ws://foo/bar/?transport=websocket&EIO=4' + ) + assert ( + c._get_engineio_url('ws://foo', 'bar', 'polling') + == 'http://foo/bar/?transport=polling&EIO=4' + ) + assert ( + c._get_engineio_url('ws://foo', 'bar', 'websocket') + == 'ws://foo/bar/?transport=websocket&EIO=4' + ) + assert ( + c._get_engineio_url('https://foo', 'bar', 'polling') + == 'https://foo/bar/?transport=polling&EIO=4' + ) + assert ( + c._get_engineio_url('https://foo', 'bar', 'websocket') + == 'wss://foo/bar/?transport=websocket&EIO=4' + ) + assert ( + c._get_engineio_url('http://foo?baz=1', 'bar', 'polling') + == 'http://foo/bar/?baz=1&transport=polling&EIO=4' + ) + assert ( + c._get_engineio_url('http://foo#baz', 'bar', 'polling') + == 'http://foo/bar/?transport=polling&EIO=4' + ) def test_read_loop_polling_disconnected(self): c = client.Client() c.state = 'disconnected' c._trigger_event = mock.MagicMock() c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._read_loop_polling() c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() c._trigger_event.assert_not_called() @mock.patch('engineio.client.time.time', return_value=123.456) @@ -863,16 +1201,14 @@ class TestClient(unittest.TestCase): c._send_request = mock.MagicMock(return_value=None) c._trigger_event = mock.MagicMock() c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._read_loop_polling() - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() - c._send_request.assert_called_once_with('GET', 'http://foo&t=123.456', - timeout=30) - c._trigger_event.assert_called_once_with('disconnect', - run_async=False) + c._send_request.assert_called_once_with( + 'GET', 'http://foo&t=123.456', timeout=30 + ) + c._trigger_event.assert_called_once_with('disconnect', run_async=False) @mock.patch('engineio.client.time.time', return_value=123.456) def test_read_loop_polling_bad_status(self, _time): @@ -885,14 +1221,13 @@ class TestClient(unittest.TestCase): c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 400 c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._read_loop_polling() - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() - c._send_request.assert_called_once_with('GET', 'http://foo&t=123.456', - timeout=30) + c._send_request.assert_called_once_with( + 'GET', 'http://foo&t=123.456', timeout=30 + ) @mock.patch('engineio.client.time.time', return_value=123.456) def test_read_loop_polling_bad_packet(self, _time): @@ -906,14 +1241,13 @@ class TestClient(unittest.TestCase): c._send_request.return_value.status_code = 200 c._send_request.return_value.content = b'foo' c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._read_loop_polling() - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() - c._send_request.assert_called_once_with('GET', 'http://foo&t=123.456', - timeout=65) + c._send_request.assert_called_once_with( + 'GET', 'http://foo&t=123.456', timeout=65 + ) def test_read_loop_polling(self): c = client.Client() @@ -924,32 +1258,45 @@ class TestClient(unittest.TestCase): c.queue = mock.MagicMock() c._send_request = mock.MagicMock() c._send_request.side_effect = [ - mock.MagicMock(status_code=200, content=payload.Payload(packets=[ - packet.Packet(packet.PING), - packet.Packet(packet.NOOP)]).encode()), - None + mock.MagicMock( + status_code=200, + content=payload.Payload( + packets=[ + packet.Packet(packet.PING), + packet.Packet(packet.NOOP), + ] + ).encode().encode('utf-8'), + ), + None, ] c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._receive_packet = mock.MagicMock() c._read_loop_polling() - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) - self.assertEqual(c._send_request.call_count, 2) - self.assertEqual(c._receive_packet.call_count, 2) - self.assertEqual(c._receive_packet.call_args_list[0][0][0].encode(), - b'2') - self.assertEqual(c._receive_packet.call_args_list[1][0][0].encode(), - b'6') + assert c._send_request.call_count == 2 + assert c._receive_packet.call_count == 2 + assert c._receive_packet.call_args_list[0][0][0].encode() == '2' + assert c._receive_packet.call_args_list[1][0][0].encode() == '6' def test_read_loop_websocket_disconnected(self): c = client.Client() c.state = 'disconnected' c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._read_loop_websocket() c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() + + def test_read_loop_websocket_timeout(self): + c = client.Client() + c.state = 'connected' + c.queue = mock.MagicMock() + c.ws = mock.MagicMock() + c.ws.recv.side_effect = websocket.WebSocketTimeoutException + c.write_loop_task = mock.MagicMock() + c._read_loop_websocket() + assert c.state == 'disconnected' + c.queue.put.assert_called_once_with(None) + c.write_loop_task.join.assert_called_once_with() def test_read_loop_websocket_no_response(self): c = client.Client() @@ -958,12 +1305,10 @@ class TestClient(unittest.TestCase): c.ws = mock.MagicMock() c.ws.recv.side_effect = websocket.WebSocketConnectionClosedException c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._read_loop_websocket() - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() def test_read_loop_websocket_unexpected_error(self): c = client.Client() @@ -972,30 +1317,29 @@ class TestClient(unittest.TestCase): c.ws = mock.MagicMock() c.ws.recv.side_effect = ValueError c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._read_loop_websocket() - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() def test_read_loop_websocket(self): c = client.Client() + c.ping_interval = 1 + c.ping_timeout = 2 c.state = 'connected' c.queue = mock.MagicMock() c.ws = mock.MagicMock() - c.ws.recv.side_effect = [packet.Packet(packet.PING).encode(), - ValueError] + c.ws.recv.side_effect = [ + packet.Packet(packet.PING).encode(), + ValueError, + ] c.write_loop_task = mock.MagicMock() - c.ping_loop_task = mock.MagicMock() c._receive_packet = mock.MagicMock() c._read_loop_websocket() - self.assertEqual(c.state, 'disconnected') + assert c.state == 'disconnected' c.queue.put.assert_called_once_with(None) c.write_loop_task.join.assert_called_once_with() - c.ping_loop_task.join.assert_called_once_with() - self.assertEqual(c._receive_packet.call_args_list[0][0][0].encode(), - b'2') + assert c._receive_packet.call_args_list[0][0][0].encode() == '2' def test_write_loop_disconnected(self): c = client.Client() @@ -1037,17 +1381,22 @@ class TestClient(unittest.TestCase): c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, - RuntimeError + RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 200 c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 1) + assert c.queue.task_done.call_count == 1 p = payload.Payload( - packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})]) + packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) c._send_request.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) def test_write_loop_polling_three_packets(self): c = client.Client() @@ -1063,20 +1412,26 @@ class TestClient(unittest.TestCase): packet.Packet(packet.PING), packet.Packet(packet.NOOP), RuntimeError, - RuntimeError + RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 200 c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 3) - p = payload.Payload(packets=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - packet.Packet(packet.PING), - packet.Packet(packet.NOOP), - ]) + assert c.queue.task_done.call_count == 3 + p = payload.Payload( + packets=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + packet.Packet(packet.PING), + packet.Packet(packet.NOOP), + ] + ) c._send_request.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) def test_write_loop_polling_two_packets_done(self): c = client.Client() @@ -1091,20 +1446,26 @@ class TestClient(unittest.TestCase): packet.Packet(packet.MESSAGE, {'foo': 'bar'}), packet.Packet(packet.PING), None, - RuntimeError + RuntimeError, ] c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 200 c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 3) - p = payload.Payload(packets=[ - packet.Packet(packet.MESSAGE, {'foo': 'bar'}), - packet.Packet(packet.PING), - ]) + assert c.queue.task_done.call_count == 3 + p = payload.Payload( + packets=[ + packet.Packet(packet.MESSAGE, {'foo': 'bar'}), + packet.Packet(packet.PING), + ] + ) c._send_request.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) - self.assertEqual(c.state, 'connected') + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) + assert c.state == 'connected' def test_write_loop_polling_bad_connection(self): c = client.Client() @@ -1122,13 +1483,18 @@ class TestClient(unittest.TestCase): c._send_request = mock.MagicMock() c._send_request.return_value = None c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 1) + assert c.queue.task_done.call_count == 1 p = payload.Payload( - packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})]) + packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) c._send_request.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) - self.assertEqual(c.state, 'connected') + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) + assert c.state == 'connected' def test_write_loop_polling_bad_status(self): c = client.Client() @@ -1146,13 +1512,18 @@ class TestClient(unittest.TestCase): c._send_request = mock.MagicMock() c._send_request.return_value.status_code = 500 c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 1) + assert c.queue.task_done.call_count == 1 p = payload.Payload( - packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})]) + packets=[packet.Packet(packet.MESSAGE, {'foo': 'bar'})] + ) c._send_request.assert_called_once_with( - 'POST', 'http://foo', body=p.encode(), - headers={'Content-Type': 'application/octet-stream'}, timeout=5) - self.assertEqual(c.state, 'disconnected') + 'POST', + 'http://foo', + body=p.encode(), + headers={'Content-Type': 'application/octet-stream'}, + timeout=5, + ) + assert c.state == 'disconnected' def test_write_loop_websocket_one_packet(self): c = client.Client() @@ -1165,13 +1536,13 @@ class TestClient(unittest.TestCase): c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, - RuntimeError + RuntimeError, ] c.ws = mock.MagicMock() c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 1) - self.assertEqual(c.ws.send.call_count, 1) - self.assertEqual(c.ws.send_binary.call_count, 0) + assert c.queue.task_done.call_count == 1 + assert c.ws.send.call_count == 1 + assert c.ws.send_binary.call_count == 0 c.ws.send.assert_called_once_with('4{"foo":"bar"}') def test_write_loop_websocket_three_packets(self): @@ -1187,16 +1558,16 @@ class TestClient(unittest.TestCase): packet.Packet(packet.PING), packet.Packet(packet.NOOP), RuntimeError, - RuntimeError + RuntimeError, ] c.ws = mock.MagicMock() c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 3) - self.assertEqual(c.ws.send.call_count, 3) - self.assertEqual(c.ws.send_binary.call_count, 0) - self.assertEqual(c.ws.send.call_args_list[0][0][0], '4{"foo":"bar"}') - self.assertEqual(c.ws.send.call_args_list[1][0][0], '2') - self.assertEqual(c.ws.send.call_args_list[2][0][0], '6') + assert c.queue.task_done.call_count == 3 + assert c.ws.send.call_count == 3 + assert c.ws.send_binary.call_count == 0 + assert c.ws.send.call_args_list[0][0][0] == '4{"foo":"bar"}' + assert c.ws.send.call_args_list[1][0][0] == '2' + assert c.ws.send.call_args_list[2][0][0] == '6' def test_write_loop_websocket_one_packet_binary(self): c = client.Client() @@ -1209,14 +1580,14 @@ class TestClient(unittest.TestCase): c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, b'foo'), RuntimeError, - RuntimeError + RuntimeError, ] c.ws = mock.MagicMock() c._write_loop() - self.assertEqual(c.queue.task_done.call_count, 1) - self.assertEqual(c.ws.send.call_count, 0) - self.assertEqual(c.ws.send_binary.call_count, 1) - c.ws.send_binary.assert_called_once_with(b'\x04foo') + assert c.queue.task_done.call_count == 1 + assert c.ws.send.call_count == 0 + assert c.ws.send_binary.call_count == 1 + c.ws.send_binary.assert_called_once_with(b'foo') def test_write_loop_websocket_bad_connection(self): c = client.Client() @@ -1229,12 +1600,12 @@ class TestClient(unittest.TestCase): c.queue.get.side_effect = [ packet.Packet(packet.MESSAGE, {'foo': 'bar'}), RuntimeError, - RuntimeError + RuntimeError, ] c.ws = mock.MagicMock() c.ws.send.side_effect = websocket.WebSocketConnectionClosedException c._write_loop() - self.assertEqual(c.state, 'connected') + assert c.state == 'connected' @mock.patch('engineio.client.original_signal_handler') def test_signal_handler(self, original_handler): @@ -1243,6 +1614,5 @@ class TestClient(unittest.TestCase): client.connected_clients[0].is_asyncio_based.return_value = False client.connected_clients[1].is_asyncio_based.return_value = True client.signal_handler('sig', 'frame') - clients[0].disconnect.assert_called_once_with(abort=True) - clients[1].start_background_task.assert_called_once_with( - clients[1].disconnect, abort=True) + clients[0].disconnect.assert_called_once_with() + clients[1].disconnect.assert_not_called() diff --git a/debian/tests/common/test_middleware.py b/debian/tests/common/test_middleware.py index 2e5a1e7..41c861d 100644 --- a/debian/tests/common/test_middleware.py +++ b/debian/tests/common/test_middleware.py @@ -1,11 +1,6 @@ import os import unittest - -import six -if six.PY3: - from unittest import mock -else: - import mock +from unittest import mock import engineio @@ -28,59 +23,72 @@ class TestWSGIApp(unittest.TestCase): environ = {'PATH_INFO': '/engine.io/'} start_response = "foo" m(environ, start_response) - mock_eio_app.handle_request.assert_called_once_with(environ, - start_response) + mock_eio_app.handle_request.assert_called_once_with( + environ, start_response + ) def test_static_files(self): root_dir = os.path.dirname(__file__) - m = engineio.WSGIApp('foo', None, static_files={ - '/': root_dir + '/index.html', - '/foo': {'content_type': 'text/plain', - 'filename': root_dir + '/index.html'}, - '/static': root_dir, - '/static/test/': root_dir + '/', - }) + m = engineio.WSGIApp( + 'foo', + None, + static_files={ + '/': root_dir + '/index.html', + '/foo': { + 'content_type': 'text/plain', + 'filename': root_dir + '/index.html', + }, + '/static': root_dir, + '/static/test/': root_dir + '/', + }, + ) def check_path(path, status_code, content_type, body): environ = {'PATH_INFO': path} start_response = mock.MagicMock() r = m(environ, start_response) - self.assertEqual(r, [body.encode('utf-8')]) + assert r == [body.encode('utf-8')] start_response.assert_called_once_with( - status_code, [('Content-Type', content_type)]) + status_code, [('Content-Type', content_type)] + ) check_path('/', '200 OK', 'text/html', '<html></html>\n') check_path('/foo', '200 OK', 'text/plain', '<html></html>\n') - check_path('/static/index.html', '200 OK', 'text/html', - '<html></html>\n') - check_path('/static/foo.bar', '404 Not Found', 'text/plain', - 'Not Found') - check_path('/static/test/index.html', '200 OK', 'text/html', - '<html></html>\n') + check_path( + '/static/index.html', '200 OK', 'text/html', '<html></html>\n' + ) + check_path( + '/static/foo.bar', '404 Not Found', 'text/plain', 'Not Found' + ) + check_path( + '/static/test/index.html', '200 OK', 'text/html', '<html></html>\n' + ) check_path('/static/test/', '200 OK', 'text/html', '<html></html>\n') check_path('/bar/foo', '404 Not Found', 'text/plain', 'Not Found') check_path('', '404 Not Found', 'text/plain', 'Not Found') m.static_files[''] = 'index.html' - check_path('/static/test/', '200 OK', 'text/html', - '<html></html>\n') + check_path('/static/test/', '200 OK', 'text/html', '<html></html>\n') m.static_files[''] = {'filename': 'index.html'} - check_path('/static/test/', '200 OK', 'text/html', - '<html></html>\n') + check_path('/static/test/', '200 OK', 'text/html', '<html></html>\n') - m.static_files[''] = {'filename': 'index.html', - 'content_type': 'image/gif'} - check_path('/static/test/', '200 OK', 'image/gif', - '<html></html>\n') + m.static_files[''] = { + 'filename': 'index.html', + 'content_type': 'image/gif', + } + check_path('/static/test/', '200 OK', 'image/gif', '<html></html>\n') m.static_files[''] = {'filename': 'test.gif'} - check_path('/static/test/', '404 Not Found', 'text/plain', - 'Not Found') + check_path('/static/test/', '404 Not Found', 'text/plain', 'Not Found') m.static_files = {} - check_path('/static/test/index.html', '404 Not Found', 'text/plain', - 'Not Found') + check_path( + '/static/test/index.html', + '404 Not Found', + 'text/plain', + 'Not Found', + ) def test_404(self): mock_wsgi_app = None @@ -89,9 +97,10 @@ class TestWSGIApp(unittest.TestCase): environ = {'PATH_INFO': '/foo/bar'} start_response = mock.MagicMock() r = m(environ, start_response) - self.assertEqual(r, [b'Not Found']) + assert r == [b'Not Found'] start_response.assert_called_once_with( - "404 Not Found", [('Content-Type', 'text/plain')]) + "404 Not Found", [('Content-Type', 'text/plain')] + ) def test_custom_eio_path(self): mock_wsgi_app = None @@ -101,26 +110,30 @@ class TestWSGIApp(unittest.TestCase): environ = {'PATH_INFO': '/engine.io/'} start_response = mock.MagicMock() r = m(environ, start_response) - self.assertEqual(r, [b'Not Found']) + assert r == [b'Not Found'] start_response.assert_called_once_with( - "404 Not Found", [('Content-Type', 'text/plain')]) + "404 Not Found", [('Content-Type', 'text/plain')] + ) environ = {'PATH_INFO': '/foo/'} m(environ, start_response) - mock_eio_app.handle_request.assert_called_once_with(environ, - start_response) + mock_eio_app.handle_request.assert_called_once_with( + environ, start_response + ) def test_custom_eio_path_slashes(self): mock_wsgi_app = None mock_eio_app = mock.Mock() mock_eio_app.handle_request = mock.MagicMock() - m = engineio.WSGIApp(mock_eio_app, mock_wsgi_app, - engineio_path='/foo/') + m = engineio.WSGIApp( + mock_eio_app, mock_wsgi_app, engineio_path='/foo/' + ) environ = {'PATH_INFO': '/foo/'} start_response = mock.MagicMock() m(environ, start_response) - mock_eio_app.handle_request.assert_called_once_with(environ, - start_response) + mock_eio_app.handle_request.assert_called_once_with( + environ, start_response + ) def test_custom_eio_path_leading_slash(self): mock_wsgi_app = None @@ -130,8 +143,9 @@ class TestWSGIApp(unittest.TestCase): environ = {'PATH_INFO': '/foo/'} start_response = mock.MagicMock() m(environ, start_response) - mock_eio_app.handle_request.assert_called_once_with(environ, - start_response) + mock_eio_app.handle_request.assert_called_once_with( + environ, start_response + ) def test_custom_eio_path_trailing_slash(self): mock_wsgi_app = None @@ -141,8 +155,9 @@ class TestWSGIApp(unittest.TestCase): environ = {'PATH_INFO': '/foo/'} start_response = mock.MagicMock() m(environ, start_response) - mock_eio_app.handle_request.assert_called_once_with(environ, - start_response) + mock_eio_app.handle_request.assert_called_once_with( + environ, start_response + ) def test_gunicorn_socket(self): mock_wsgi_app = None @@ -151,12 +166,12 @@ class TestWSGIApp(unittest.TestCase): environ = {'gunicorn.socket': 123, 'PATH_INFO': '/foo/bar'} start_response = mock.MagicMock() m(environ, start_response) - self.assertIn('eventlet.input', environ) - self.assertEqual(environ['eventlet.input'].get_socket(), 123) + assert 'eventlet.input' in environ + assert environ['eventlet.input'].get_socket() == 123 def test_legacy_middleware_class(self): m = engineio.Middleware('eio', 'wsgi', 'eio_path') - self.assertEqual(m.engineio_app, 'eio') - self.assertEqual(m.wsgi_app, 'wsgi') - self.assertEqual(m.static_files, {}) - self.assertEqual(m.engineio_path, 'eio_path') + assert m.engineio_app == 'eio' + assert m.wsgi_app == 'wsgi' + assert m.static_files == {} + assert m.engineio_path == 'eio_path' diff --git a/debian/tests/common/test_packet.py b/debian/tests/common/test_packet.py index 15d6305..9a9b84c 100644 --- a/debian/tests/common/test_packet.py +++ b/debian/tests/common/test_packet.py @@ -1,6 +1,6 @@ import unittest -import six +import pytest from engineio import packet @@ -8,96 +8,94 @@ from engineio import packet class TestPacket(unittest.TestCase): def test_encode_default_packet(self): pkt = packet.Packet() - self.assertEqual(pkt.packet_type, packet.NOOP) - self.assertIsNone(pkt.data) - self.assertFalse(pkt.binary) - self.assertEqual(pkt.encode(), b'6') + assert pkt.packet_type == packet.NOOP + assert pkt.data is None + assert not pkt.binary + assert pkt.encode() == '6' def test_decode_default_packet(self): - pkt = packet.Packet(encoded_packet=b'6') - self.assertTrue(pkt.encode(), b'6') + pkt = packet.Packet(encoded_packet='6') + assert pkt.encode() == '6' def test_encode_text_packet(self): - data = six.text_type('text') + data = 'text' pkt = packet.Packet(packet.MESSAGE, data=data) - self.assertEqual(pkt.packet_type, packet.MESSAGE) - self.assertEqual(pkt.data, data) - self.assertFalse(pkt.binary) - self.assertEqual(pkt.encode(), b'4text') + assert pkt.packet_type == packet.MESSAGE + assert pkt.data == data + assert not pkt.binary + assert pkt.encode() == '4text' def test_decode_text_packet(self): pkt = packet.Packet(encoded_packet=b'4text') - self.assertEqual(pkt.encode(), b'4text') + assert pkt.encode() == b'4text' def test_encode_binary_packet(self): - pkt = packet.Packet(packet.MESSAGE, data=b'\x01\x02\x03', - binary=True) - self.assertEqual(pkt.packet_type, packet.MESSAGE) - self.assertEqual(pkt.data, b'\x01\x02\x03') - self.assertTrue(pkt.binary) - self.assertEqual(pkt.encode(), b'\x04\x01\x02\x03') + pkt = packet.Packet(packet.MESSAGE, data=b'\x01\x02\x03') + assert pkt.packet_type == packet.MESSAGE + assert pkt.data == b'\x01\x02\x03' + assert pkt.binary + assert pkt.encode() == b'\x01\x02\x03' def test_encode_binary_bytearray_packet(self): - pkt = packet.Packet(packet.MESSAGE, data=bytearray(b'\x01\x02\x03'), - binary=True) - self.assertEqual(pkt.packet_type, packet.MESSAGE) - self.assertEqual(pkt.data, b'\x01\x02\x03') - self.assertTrue(pkt.binary) - self.assertEqual(pkt.encode(), b'\x04\x01\x02\x03') + pkt = packet.Packet(packet.MESSAGE, data=bytearray(b'\x01\x02\x03')) + assert pkt.packet_type == packet.MESSAGE + assert pkt.data == b'\x01\x02\x03' + assert pkt.binary + assert pkt.encode() == b'\x01\x02\x03' def test_encode_binary_b64_packet(self): - pkt = packet.Packet(packet.MESSAGE, data=b'\x01\x02\x03\x04', - binary=True) - self.assertEqual(pkt.packet_type, packet.MESSAGE) - self.assertEqual(pkt.data, b'\x01\x02\x03\x04') - self.assertTrue(pkt.binary) - self.assertEqual(pkt.encode(b64=True), b'b4AQIDBA==') - - def test_encode_binary_packet_py3(self): - pkt = packet.Packet(packet.MESSAGE, data=b'\x01\x02\x03') - self.assertEqual(pkt.packet_type, packet.MESSAGE) - self.assertEqual(pkt.data, b'\x01\x02\x03') - self.assertTrue(pkt.binary) - self.assertEqual(pkt.encode(), b'\x04\x01\x02\x03') + pkt = packet.Packet(packet.MESSAGE, data=b'\x01\x02\x03\x04') + assert pkt.packet_type == packet.MESSAGE + assert pkt.data == b'\x01\x02\x03\x04' + assert pkt.binary + assert pkt.encode(b64=True) == 'bAQIDBA==' def test_decode_binary_packet(self): pkt = packet.Packet(encoded_packet=b'\x04\x01\x02\x03') - self.assertTrue(pkt.encode(), b'\x04\x01\x02\x03') + assert pkt.encode(), b'\x04\x01\x02\x03' def test_decode_binary_bytearray_packet(self): pkt = packet.Packet(encoded_packet=bytearray(b'\x04\x01\x02\x03')) - self.assertTrue(pkt.encode(), b'\x04\x01\x02\x03') + assert pkt.encode(), b'\x04\x01\x02\x03' def test_decode_binary_b64_packet(self): pkt = packet.Packet(encoded_packet=b'b4AAEC') - self.assertTrue(pkt.encode(), b'\x04\x01\x02\x03') + assert pkt.encode(), b'\x04\x01\x02\x03' def test_encode_json_packet(self): pkt = packet.Packet(packet.MESSAGE, data={'a': 123, 'b': '456'}) - self.assertEqual(pkt.packet_type, packet.MESSAGE) - self.assertEqual(pkt.data, {'a': 123, 'b': '456'}) - self.assertFalse(pkt.binary) - self.assertIn(pkt.encode(), [b'4{"a":123,"b":"456"}', - b'4{"b":"456","a":123}']) + assert pkt.packet_type == packet.MESSAGE + assert pkt.data == {'a': 123, 'b': '456'} + assert not pkt.binary + assert pkt.encode() in [ + '4{"a":123,"b":"456"}', + '4{"b":"456","a":123}', + ] def test_decode_json_packet(self): pkt = packet.Packet(encoded_packet=b'4{"a":123,"b":"456"}') - self.assertIn(pkt.encode(), [b'4{"a":123,"b":"456"}', - b'4{"b":"456","a":123}']) + assert pkt.encode() in [ + b'4{"a":123,"b":"456"}', + b'4{"b":"456","a":123}', + ] def test_encode_number_packet(self): pkt = packet.Packet(packet.MESSAGE, data=123) - self.assertEqual(pkt.packet_type, packet.MESSAGE) - self.assertEqual(pkt.data, 123) - self.assertFalse(pkt.binary) - self.assertEqual(pkt.encode(), b'4123') + assert pkt.packet_type == packet.MESSAGE + assert pkt.data == 123 + assert not pkt.binary + assert pkt.encode() == '4123' def test_decode_number_packet(self): - pkt = packet.Packet(encoded_packet=b'4123') - self.assertEqual(pkt.packet_type, packet.MESSAGE) + pkt = packet.Packet(encoded_packet='4123') + assert pkt.packet_type == packet.MESSAGE # integer payloads are parsed as strings, see # https://github.com/miguelgrinberg/python-engineio/issues/75 # for background on this decision - self.assertEqual(pkt.data, '123') - self.assertFalse(pkt.binary) - self.assertEqual(pkt.encode(), b'4123') + assert pkt.data == '123' + assert not pkt.binary + assert pkt.encode() == '4123' + + def test_binary_non_message_packet(self): + with pytest.raises(ValueError): + packet.Packet(packet.NOOP, b'\x01\x02\x03') diff --git a/debian/tests/common/test_payload.py b/debian/tests/common/test_payload.py index 20dad1b..06c3289 100644 --- a/debian/tests/common/test_payload.py +++ b/debian/tests/common/test_payload.py @@ -1,6 +1,6 @@ import unittest -import six +import pytest from engineio import packet from engineio import payload @@ -9,88 +9,60 @@ from engineio import payload class TestPayload(unittest.TestCase): def test_encode_empty_payload(self): p = payload.Payload() - self.assertEqual(p.packets, []) - self.assertEqual(p.encode(), b'') + assert p.packets == [] + assert p.encode() == '' def test_decode_empty_payload(self): - p = payload.Payload(encoded_payload=b'') - self.assertEqual(p.encode(), b'') + p = payload.Payload(encoded_payload='') + assert p.encode() == '' - def test_encode_payload_xhr2(self): - pkt = packet.Packet(packet.MESSAGE, data=six.text_type('abc')) + def test_encode_payload_text(self): + pkt = packet.Packet(packet.MESSAGE, data='abc') p = payload.Payload([pkt]) - self.assertEqual(p.packets, [pkt]) - self.assertEqual(p.encode(), b'\x00\x04\xff4abc') - - def test_decode_payload_xhr2(self): - p = payload.Payload(encoded_payload=b'\x00\x04\xff4abc') - self.assertEqual(p.encode(), b'\x00\x04\xff4abc') - - def test_encode_payload_xhr_text(self): - pkt = packet.Packet(packet.MESSAGE, data=six.text_type('abc')) - p = payload.Payload([pkt]) - self.assertEqual(p.packets, [pkt]) - self.assertEqual(p.encode(b64=True), b'4:4abc') - - def test_decode_payload_xhr_text(self): - p = payload.Payload(encoded_payload=b'4:4abc') - self.assertEqual(p.encode(), b'\x00\x04\xff4abc') - - def test_encode_payload_xhr_binary(self): - pkt = packet.Packet(packet.MESSAGE, data=b'\x00\x01\x02', binary=True) + assert p.packets == [pkt] + assert p.encode() == '4abc' + + def test_encode_payload_text_multiple(self): + pkt = packet.Packet(packet.MESSAGE, data='abc') + pkt2 = packet.Packet(packet.MESSAGE, data='def') + p = payload.Payload([pkt, pkt2]) + assert p.packets == [pkt, pkt2] + assert p.encode() == '4abc\x1e4def' + + def test_encode_payload_binary(self): + pkt = packet.Packet(packet.MESSAGE, data=b'\x00\x01\x02') p = payload.Payload([pkt]) - self.assertEqual(p.packets, [pkt]) - self.assertEqual(p.encode(b64=True), b'6:b4AAEC') - - def test_decode_payload_xhr_binary(self): - p = payload.Payload(encoded_payload=b'6:b4AAEC') - self.assertEqual(p.encode(), b'\x01\x04\xff\x04\x00\x01\x02') + assert p.packets == [pkt] + assert p.encode() == 'bAAEC' + + def test_encode_payload_binary_multiple(self): + pkt = packet.Packet(packet.MESSAGE, data=b'\x00\x01\x02') + pkt2 = packet.Packet(packet.MESSAGE, data=b'\x03\x04\x05\x06') + p = payload.Payload([pkt, pkt2]) + assert p.packets == [pkt, pkt2] + assert p.encode() == 'bAAEC\x1ebAwQFBg==' + + def test_encode_payload_text_binary_multiple(self): + pkt = packet.Packet(packet.MESSAGE, data='abc') + pkt2 = packet.Packet(packet.MESSAGE, data=b'\x03\x04\x05\x06') + p = payload.Payload([pkt, pkt2, pkt2, pkt]) + assert p.packets == [pkt, pkt2, pkt2, pkt] + assert p.encode() == '4abc\x1ebAwQFBg==\x1ebAwQFBg==\x1e4abc' def test_encode_jsonp_payload(self): - pkt = packet.Packet(packet.MESSAGE, data=six.text_type('abc')) + pkt = packet.Packet(packet.MESSAGE, data='abc') p = payload.Payload([pkt]) - self.assertEqual(p.packets, [pkt]) - self.assertEqual(p.encode(jsonp_index=233), - b'___eio[233]("\x00\x04\xff4abc");') - self.assertEqual(p.encode(jsonp_index=233, b64=True), - b'___eio[233]("4:4abc");') + assert p.packets == [pkt] + assert p.encode(jsonp_index=233) == '___eio[233]("4abc");' def test_decode_jsonp_payload(self): - p = payload.Payload(encoded_payload=b'd=4:4abc') - self.assertEqual(p.encode(), b'\x00\x04\xff4abc') + p = payload.Payload(encoded_payload='d=4abc') + assert p.encode() == '4abc' def test_decode_invalid_payload(self): - self.assertRaises(ValueError, payload.Payload, - encoded_payload=b'bad payload') - - # performance improvements in the payload parser assume packets in a - # payload are either all binary or all text, so the following test does - # no work anymore. - # - # def test_decode_multi_payload(self): - # p = payload.Payload(encoded_payload=b'4:4abc\x00\x04\xff4def') - # self.assertEqual(len(p.packets), 2) - # self.assertEqual(p.packets[0].data, 'abc') - # self.assertEqual(p.packets[1].data, 'def') - - def test_decode_multi_binary_payload(self): - p = payload.Payload( - encoded_payload=b'\x00\x04\xff4abc\x00\x04\xff4def') - self.assertEqual(len(p.packets), 2) - self.assertEqual(p.packets[0].data, 'abc') - self.assertEqual(p.packets[1].data, 'def') - - def test_decode_multi_text_payload(self): - p = payload.Payload(encoded_payload=b'4:4abc4:4def') - self.assertEqual(len(p.packets), 2) - self.assertEqual(p.packets[0].data, 'abc') - self.assertEqual(p.packets[1].data, 'def') - - def test_decode_multi_binary_payload_with_too_many_packets(self): - self.assertRaises( - ValueError, payload.Payload, - encoded_payload=b'\x00\x04\xff4abc\x00\x04\xff4def' * 9) + with pytest.raises(ValueError): + payload.Payload(encoded_payload='bad payload') - def test_decode_multi_text_payload_with_too_many_packets(self): - self.assertRaises(ValueError, payload.Payload, - encoded_payload=b'4:4abc4:4def' * 9) + def test_decode_multi_payload_with_too_many_packets(self): + with pytest.raises(ValueError): + payload.Payload(encoded_payload='4abc\x1e4def\x1e' * 9 + '6') diff --git a/debian/tests/common/test_server.py b/debian/tests/common/test_server.py index 550e31c..9082100 100644 --- a/debian/tests/common/test_server.py +++ b/debian/tests/common/test_server.py @@ -1,24 +1,21 @@ import gzip import importlib +import io import json import logging import sys import time import unittest +from unittest import mock import zlib -import six -if six.PY3: - from unittest import mock -else: - import mock +import pytest from engineio import exceptions from engineio import packet from engineio import payload from engineio import server - original_import_module = importlib.import_module @@ -62,12 +59,16 @@ class TestServer(unittest.TestCase): def test_is_asyncio_based(self): s = server.Server() - self.assertEqual(s.is_asyncio_based(), False) + assert not s.is_asyncio_based() def test_async_modes(self): s = server.Server() - self.assertEqual(s.async_modes(), ['eventlet', 'gevent_uwsgi', - 'gevent', 'threading']) + assert s.async_modes() == [ + 'eventlet', + 'gevent_uwsgi', + 'gevent', + 'threading', + ] def test_create(self): kwargs = { @@ -80,45 +81,47 @@ class TestServer(unittest.TestCase): 'cookie': 'foo', 'cors_allowed_origins': ['foo', 'bar', 'baz'], 'cors_credentials': False, - 'async_handlers': False} + 'async_handlers': False, + } s = server.Server(**kwargs) - for arg in six.iterkeys(kwargs): - self.assertEqual(getattr(s, arg), kwargs[arg]) - self.assertEqual(s.ping_interval_grace_period, 5) + for arg in kwargs.keys(): + assert getattr(s, arg) == kwargs[arg] + assert s.ping_interval_grace_period == 0 def test_create_with_grace_period(self): s = server.Server(ping_interval=(1, 2)) - self.assertEqual(s.ping_interval, 1) - self.assertEqual(s.ping_interval_grace_period, 2) + assert s.ping_interval == 1 + assert s.ping_interval_grace_period == 2 def test_create_ignores_kwargs(self): server.Server(foo='bar') # this should not raise def test_async_mode_threading(self): s = server.Server(async_mode='threading') - self.assertEqual(s.async_mode, 'threading') + assert s.async_mode == 'threading' import threading + try: import queue except ImportError: import Queue as queue - self.assertEqual(s._async['thread'], threading.Thread) - self.assertEqual(s._async['queue'], queue.Queue) - self.assertEqual(s._async['websocket'], None) + assert s._async['thread'] == threading.Thread + assert s._async['queue'] == queue.Queue + assert s._async['websocket'] is None def test_async_mode_eventlet(self): s = server.Server(async_mode='eventlet') - self.assertEqual(s.async_mode, 'eventlet') + assert s.async_mode == 'eventlet' from eventlet.green import threading from eventlet import queue from engineio.async_drivers import eventlet as async_eventlet - self.assertEqual(s._async['thread'], threading.Thread) - self.assertEqual(s._async['queue'], queue.Queue) - self.assertEqual(s._async['websocket'], async_eventlet.WebSocketWSGI) + assert s._async['thread'] == threading.Thread + assert s._async['queue'] == queue.Queue + assert s._async['websocket'] == async_eventlet.WebSocketWSGI @mock.patch('importlib.import_module', side_effect=_mock_import) def test_async_mode_gevent_uwsgi(self, import_module): @@ -131,16 +134,15 @@ class TestServer(unittest.TestCase): sys.modules['gevent.event'].Event = 'bar' sys.modules['uwsgi'] = mock.MagicMock() s = server.Server(async_mode='gevent_uwsgi') - self.assertEqual(s.async_mode, 'gevent_uwsgi') + assert s.async_mode == 'gevent_uwsgi' from engineio.async_drivers import gevent_uwsgi as async_gevent_uwsgi - self.assertEqual(s._async['thread'], async_gevent_uwsgi.Thread) - self.assertEqual(s._async['queue'], 'foo') - self.assertEqual(s._async['queue_empty'], RuntimeError) - self.assertEqual(s._async['event'], 'bar') - self.assertEqual(s._async['websocket'], - async_gevent_uwsgi.uWSGIWebSocket) + assert s._async['thread'] == async_gevent_uwsgi.Thread + assert s._async['queue'] == 'foo' + assert s._async['queue_empty'] == RuntimeError + assert s._async['event'] == 'bar' + assert s._async['websocket'] == async_gevent_uwsgi.uWSGIWebSocket del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] @@ -157,8 +159,8 @@ class TestServer(unittest.TestCase): sys.modules['gevent.event'] = mock.MagicMock() sys.modules['gevent.event'].Event = 'bar' sys.modules['uwsgi'] = None - self.assertRaises(ValueError, server.Server, - async_mode='gevent_uwsgi') + with pytest.raises(ValueError): + server.Server(async_mode='gevent_uwsgi') del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] @@ -176,15 +178,15 @@ class TestServer(unittest.TestCase): sys.modules['uwsgi'] = mock.MagicMock() del sys.modules['uwsgi'].websocket_handshake s = server.Server(async_mode='gevent_uwsgi') - self.assertEqual(s.async_mode, 'gevent_uwsgi') + assert s.async_mode == 'gevent_uwsgi' from engineio.async_drivers import gevent_uwsgi as async_gevent_uwsgi - self.assertEqual(s._async['thread'], async_gevent_uwsgi.Thread) - self.assertEqual(s._async['queue'], 'foo') - self.assertEqual(s._async['queue_empty'], RuntimeError) - self.assertEqual(s._async['event'], 'bar') - self.assertEqual(s._async['websocket'], None) + assert s._async['thread'] == async_gevent_uwsgi.Thread + assert s._async['queue'] == 'foo' + assert s._async['queue_empty'] == RuntimeError + assert s._async['event'] == 'bar' + assert s._async['websocket'] is None del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] @@ -202,15 +204,15 @@ class TestServer(unittest.TestCase): sys.modules['gevent.event'].Event = 'bar' sys.modules['geventwebsocket'] = 'geventwebsocket' s = server.Server(async_mode='gevent') - self.assertEqual(s.async_mode, 'gevent') + assert s.async_mode == 'gevent' from engineio.async_drivers import gevent as async_gevent - self.assertEqual(s._async['thread'], async_gevent.Thread) - self.assertEqual(s._async['queue'], 'foo') - self.assertEqual(s._async['queue_empty'], RuntimeError) - self.assertEqual(s._async['event'], 'bar') - self.assertEqual(s._async['websocket'], async_gevent.WebSocketWSGI) + assert s._async['thread'] == async_gevent.Thread + assert s._async['queue'] == 'foo' + assert s._async['queue_empty'] == RuntimeError + assert s._async['event'] == 'bar' + assert s._async['websocket'] == async_gevent.WebSocketWSGI del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] @@ -228,15 +230,15 @@ class TestServer(unittest.TestCase): sys.modules['gevent.event'].Event = 'bar' sys.modules['geventwebsocket'] = None s = server.Server(async_mode='gevent') - self.assertEqual(s.async_mode, 'gevent') + assert s.async_mode == 'gevent' from engineio.async_drivers import gevent as async_gevent - self.assertEqual(s._async['thread'], async_gevent.Thread) - self.assertEqual(s._async['queue'], 'foo') - self.assertEqual(s._async['queue_empty'], RuntimeError) - self.assertEqual(s._async['event'], 'bar') - self.assertEqual(s._async['websocket'], None) + assert s._async['thread'] == async_gevent.Thread + assert s._async['queue'] == 'foo' + assert s._async['queue_empty'] == RuntimeError + assert s._async['event'] == 'bar' + assert s._async['websocket'] is None del sys.modules['gevent'] del sys.modules['gevent.queue'] del sys.modules['gevent.event'] @@ -247,41 +249,45 @@ class TestServer(unittest.TestCase): @mock.patch('importlib.import_module', side_effect=_mock_import) def test_async_mode_aiohttp(self, import_module): sys.modules['aiohttp'] = mock.MagicMock() - self.assertRaises(ValueError, server.Server, async_mode='aiohttp') + with pytest.raises(ValueError): + server.Server(async_mode='aiohttp') @mock.patch('importlib.import_module', side_effect=[ImportError]) def test_async_mode_invalid(self, import_module): - self.assertRaises(ValueError, server.Server, async_mode='foo') + with pytest.raises(ValueError): + server.Server(async_mode='foo') @mock.patch('importlib.import_module', side_effect=[_mock_async]) def test_async_mode_auto_eventlet(self, import_module): s = server.Server() - self.assertEqual(s.async_mode, 'eventlet') + assert s.async_mode == 'eventlet' - @mock.patch('importlib.import_module', side_effect=[ImportError, - _mock_async]) + @mock.patch( + 'importlib.import_module', side_effect=[ImportError, _mock_async] + ) def test_async_mode_auto_gevent_uwsgi(self, import_module): s = server.Server() - self.assertEqual(s.async_mode, 'gevent_uwsgi') + assert s.async_mode == 'gevent_uwsgi' - @mock.patch('importlib.import_module', side_effect=[ImportError, - ImportError, - _mock_async]) + @mock.patch( + 'importlib.import_module', + side_effect=[ImportError, ImportError, _mock_async], + ) def test_async_mode_auto_gevent(self, import_module): s = server.Server() - self.assertEqual(s.async_mode, 'gevent') + assert s.async_mode == 'gevent' - @mock.patch('importlib.import_module', side_effect=[ImportError, - ImportError, - ImportError, - _mock_async]) + @mock.patch( + 'importlib.import_module', + side_effect=[ImportError, ImportError, ImportError, _mock_async], + ) def test_async_mode_auto_threading(self, import_module): s = server.Server() - self.assertEqual(s.async_mode, 'threading') + assert s.async_mode == 'threading' def test_generate_id(self): s = server.Server() - self.assertNotEqual(s._generate_id(), s._generate_id()) + assert s.generate_id() != s.generate_id() def test_on_event(self): s = server.Server() @@ -289,14 +295,16 @@ class TestServer(unittest.TestCase): @s.on('connect') def foo(): pass + s.on('disconnect', foo) - self.assertEqual(s.handlers['connect'], foo) - self.assertEqual(s.handlers['disconnect'], foo) + assert s.handlers['connect'] == foo + assert s.handlers['disconnect'] == foo def test_on_event_invalid(self): s = server.Server() - self.assertRaises(ValueError, s.on, 'invalid') + with pytest.raises(ValueError): + s.on('invalid') def test_trigger_event(self): s = server.Server() @@ -312,12 +320,12 @@ class TestServer(unittest.TestCase): return 'bar' r = s._trigger_event('connect', 1, 2, run_async=False) - self.assertEqual(r, 3) + assert r == 3 r = s._trigger_event('message', 3, 4, run_async=True) r.join() - self.assertEqual(f['bar'], 7) + assert f['bar'] == 7 r = s._trigger_event('message', 5, 6) - self.assertEqual(r, 'bar') + assert r == 'bar' def test_trigger_event_error(self): s = server.Server() @@ -331,26 +339,26 @@ class TestServer(unittest.TestCase): return 1 / 0 r = s._trigger_event('connect', 1, 2, run_async=False) - self.assertEqual(r, False) + assert not r r = s._trigger_event('message', 3, 4, run_async=False) - self.assertEqual(r, None) + assert r is None def test_session(self): s = server.Server() mock_socket = self._get_mock_socket() s.sockets['foo'] = mock_socket with s.session('foo') as session: - self.assertEqual(session, {}) + assert session == {} session['username'] = 'bar' - self.assertEqual(s.get_session('foo'), {'username': 'bar'}) + assert s.get_session('foo') == {'username': 'bar'} def test_close_one_socket(self): s = server.Server() mock_socket = self._get_mock_socket() s.sockets['foo'] = mock_socket s.disconnect('foo') - self.assertEqual(mock_socket.close.call_count, 1) - self.assertNotIn('foo', s.sockets) + assert mock_socket.close.call_count == 1 + assert 'foo' not in s.sockets def test_close_all_sockets(self): s = server.Server() @@ -359,22 +367,22 @@ class TestServer(unittest.TestCase): mock_sockets[sid] = self._get_mock_socket() s.sockets[sid] = mock_sockets[sid] s.disconnect() - for socket in six.itervalues(mock_sockets): - self.assertEqual(socket.close.call_count, 1) - self.assertEqual(s.sockets, {}) + for socket in mock_sockets.values(): + assert socket.close.call_count == 1 + assert s.sockets == {} def test_upgrades(self): s = server.Server() s.sockets['foo'] = self._get_mock_socket() - self.assertEqual(s._upgrades('foo', 'polling'), ['websocket']) - self.assertEqual(s._upgrades('foo', 'websocket'), []) + assert s._upgrades('foo', 'polling') == ['websocket'] + assert s._upgrades('foo', 'websocket') == [] s.sockets['foo'].upgraded = True - self.assertEqual(s._upgrades('foo', 'polling'), []) - self.assertEqual(s._upgrades('foo', 'websocket'), []) + assert s._upgrades('foo', 'polling') == [] + assert s._upgrades('foo', 'websocket') == [] s.allow_upgrades = False s.sockets['foo'].upgraded = True - self.assertEqual(s._upgrades('foo', 'polling'), []) - self.assertEqual(s._upgrades('foo', 'websocket'), []) + assert s._upgrades('foo', 'polling') == [] + assert s._upgrades('foo', 'websocket') == [] def test_transport(self): s = server.Server() @@ -382,160 +390,141 @@ class TestServer(unittest.TestCase): s.sockets['foo'].upgraded = False s.sockets['bar'] = self._get_mock_socket() s.sockets['bar'].upgraded = True - self.assertEqual(s.transport('foo'), 'polling') - self.assertEqual(s.transport('bar'), 'websocket') + assert s.transport('foo') == 'polling' + assert s.transport('bar') == 'websocket' def test_bad_session(self): s = server.Server() s.sockets['foo'] = 'client' - self.assertRaises(KeyError, s._get_socket, 'bar') + with pytest.raises(KeyError): + s._get_socket('bar') def test_closed_socket(self): s = server.Server() s.sockets['foo'] = self._get_mock_socket() s.sockets['foo'].closed = True - self.assertRaises(KeyError, s._get_socket, 'foo') + with pytest.raises(KeyError): + s._get_socket('foo') def test_jsonp_with_bad_index(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'j=abc'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&j=abc'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') + assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_jsonp_index(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'j=233'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&j=233'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') - self.assertTrue(r[0].startswith(b'___eio[233]("')) - self.assertTrue(r[0].endswith(b'");')) + assert start_response.call_args[0][0] == '200 OK' + assert r[0].startswith(b'___eio[233]("') + assert r[0].endswith(b'");') def test_connect(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) - self.assertEqual(len(s.sockets), 1) - self.assertEqual(start_response.call_count, 1) - self.assertEqual(start_response.call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'application/octet-stream'), - start_response.call_args[0][1]) - self.assertEqual(len(r), 1) - packets = payload.Payload(encoded_payload=r[0]).packets - self.assertEqual(len(packets), 1) - self.assertEqual(packets[0].packet_type, packet.OPEN) - self.assertIn('upgrades', packets[0].data) - self.assertEqual(packets[0].data['upgrades'], ['websocket']) - self.assertIn('sid', packets[0].data) + assert len(s.sockets) == 1 + assert start_response.call_count == 1 + assert start_response.call_args[0][0] == '200 OK' + assert ( + 'Content-Type', + 'text/plain; charset=UTF-8', + ) in start_response.call_args[0][1] + assert len(r) == 1 + packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets + assert len(packets) == 1 + assert packets[0].packet_type == packet.OPEN + assert 'upgrades' in packets[0].data + assert packets[0].data['upgrades'] == ['websocket'] + assert 'sid' in packets[0].data def test_connect_no_upgrades(self): s = server.Server(allow_upgrades=False) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} - start_response = mock.MagicMock() - r = s.handle_request(environ, start_response) - packets = payload.Payload(encoded_payload=r[0]).packets - self.assertEqual(packets[0].data['upgrades'], []) - - def test_connect_b64_with_1(self): - s = server.Server(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'b64=1'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() - s.handle_request(environ, start_response) - self.assertTrue(start_response.call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'text/plain; charset=UTF-8'), - start_response.call_args[0][1]) - s.send('1', b'\x00\x01\x02', binary=True) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=1'} - r = s.handle_request(environ, start_response) - self.assertEqual(r[0], b'6:b4AAEC') - - def test_connect_b64_with_true(self): - s = server.Server(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'b64=true'} - start_response = mock.MagicMock() - s.handle_request(environ, start_response) - self.assertTrue(start_response.call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'text/plain; charset=UTF-8'), - start_response.call_args[0][1]) - s.send('1', b'\x00\x01\x02', binary=True) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=true'} r = s.handle_request(environ, start_response) - self.assertEqual(r[0], b'6:b4AAEC') + packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets + assert packets[0].data['upgrades'] == [] - def test_connect_b64_with_0(self): - s = server.Server(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'b64=0'} - start_response = mock.MagicMock() - s.handle_request(environ, start_response) - self.assertTrue(start_response.call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'application/octet-stream'), - start_response.call_args[0][1]) - s.send('1', b'\x00\x01\x02', binary=True) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=0'} - r = s.handle_request(environ, start_response) - self.assertEqual(r[0], b'\x01\x04\xff\x04\x00\x01\x02') - - def test_connect_b64_with_false(self): - s = server.Server(allow_upgrades=False) - s._generate_id = mock.MagicMock(return_value='1') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'b64=false'} + def test_connect_bad_eio_version(self): + s = server.Server() + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=1'} start_response = mock.MagicMock() - s.handle_request(environ, start_response) - self.assertTrue(start_response.call_args[0][0], '200 OK') - self.assertIn(('Content-Type', 'application/octet-stream'), - start_response.call_args[0][1]) - s.send('1', b'\x00\x01\x02', binary=True) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=1&b64=false'} r = s.handle_request(environ, start_response) - self.assertEqual(r[0], b'\x01\x04\xff\x04\x00\x01\x02') + assert start_response.call_args[0][0], '400 BAD REQUEST' + assert b'unsupported version' in r[0] def test_connect_custom_ping_times(self): s = server.Server(ping_timeout=123, ping_interval=456) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) - packets = payload.Payload(encoded_payload=r[0]).packets - self.assertEqual(packets[0].data['pingTimeout'], 123000) - self.assertEqual(packets[0].data['pingInterval'], 456000) + packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets + assert packets[0].data['pingTimeout'] == 123000 + assert packets[0].data['pingInterval'] == 456000 - @mock.patch('engineio.socket.Socket.poll', - side_effect=exceptions.QueueEmpty) + @mock.patch( + 'engineio.socket.Socket.poll', side_effect=exceptions.QueueEmpty + ) def test_connect_bad_poll(self, poll): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') + assert start_response.call_args[0][0] == '400 BAD REQUEST' - @mock.patch('engineio.socket.Socket', - return_value=mock.MagicMock(connected=False, closed=False)) + @mock.patch( + 'engineio.socket.Socket', + return_value=mock.MagicMock(connected=False, closed=False), + ) def test_connect_transport_websocket(self, Socket): s = server.Server() - s._generate_id = mock.MagicMock(return_value='123') - environ = {'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'transport=websocket'} + s.generate_id = mock.MagicMock(return_value='123') + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&transport=websocket', + 'HTTP_UPGRADE': 'websocket', + } + start_response = mock.MagicMock() + # force socket to stay open, so that we can check it later + Socket().closed = False + s.handle_request(environ, start_response) + assert s.sockets['123'].send.call_args[0][0].packet_type == packet.OPEN + + @mock.patch( + 'engineio.socket.Socket', + return_value=mock.MagicMock(connected=False, closed=False), + ) + def test_http_upgrade_case_insensitive(self, Socket): + s = server.Server() + s.generate_id = mock.MagicMock(return_value='123') + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&transport=websocket', + 'HTTP_UPGRADE': 'WebSocket', + } start_response = mock.MagicMock() # force socket to stay open, so that we can check it later Socket().closed = False s.handle_request(environ, start_response) - self.assertEqual(s.sockets['123'].send.call_args[0][0].packet_type, - packet.OPEN) + assert s.sockets['123'].send.call_args[0][0].packet_type == packet.OPEN - @mock.patch('engineio.socket.Socket', - return_value=mock.MagicMock(connected=False, closed=False)) + @mock.patch( + 'engineio.socket.Socket', + return_value=mock.MagicMock(connected=False, closed=False), + ) def test_connect_transport_websocket_closed(self, Socket): s = server.Server() - s._generate_id = mock.MagicMock(return_value='123') - environ = {'REQUEST_METHOD': 'GET', - 'QUERY_STRING': 'transport=websocket'} + s.generate_id = mock.MagicMock(return_value='123') + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&transport=websocket', + 'HTTP_UPGRADE': 'websocket', + } start_response = mock.MagicMock() def mock_handle(environ, start_response): @@ -543,239 +532,275 @@ class TestServer(unittest.TestCase): Socket().handle_get_request = mock_handle s.handle_request(environ, start_response) - self.assertNotIn('123', s.sockets) + assert '123' not in s.sockets def test_connect_transport_invalid(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'transport=foo'} + environ = {'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&transport=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') + assert start_response.call_args[0][0] == '400 BAD REQUEST' + + def test_connect_transport_websocket_without_upgrade(self): + s = server.Server() + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&transport=websocket', + } + start_response = mock.MagicMock() + s.handle_request(environ, start_response) + assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_connect_cors_headers(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Credentials', 'true') in headers def test_connect_cors_allowed_origin(self): s = server.Server(cors_allowed_origins=['a', 'b']) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'b'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'HTTP_ORIGIN': 'b', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'b'), headers) + assert ('Access-Control-Allow-Origin', 'b') in headers def test_connect_cors_not_allowed_origin(self): s = server.Server(cors_allowed_origins=['a', 'b']) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'c'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'HTTP_ORIGIN': 'c', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') + assert start_response.call_args[0][0] == '400 BAD REQUEST' headers = start_response.call_args[0][1] - self.assertNotIn(('Access-Control-Allow-Origin', 'c'), headers) - self.assertNotIn(('Access-Control-Allow-Origin', '*'), headers) + assert ('Access-Control-Allow-Origin', 'c') not in headers + assert ('Access-Control-Allow-Origin', '*') not in headers def test_connect_cors_headers_all_origins(self): s = server.Server(cors_allowed_origins='*') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'foo'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'HTTP_ORIGIN': 'foo', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'foo'), headers) - self.assertIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Origin', 'foo') in headers + assert ('Access-Control-Allow-Credentials', 'true') in headers def test_connect_cors_headers_one_origin(self): s = server.Server(cors_allowed_origins='a') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'a'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'HTTP_ORIGIN': 'a', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'a'), headers) - self.assertIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Origin', 'a') in headers + assert ('Access-Control-Allow-Credentials', 'true') in headers def test_connect_cors_headers_one_origin_not_allowed(self): s = server.Server(cors_allowed_origins='a') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'b'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'HTTP_ORIGIN': 'b', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') + assert start_response.call_args[0][0] == '400 BAD REQUEST' headers = start_response.call_args[0][1] - self.assertNotIn(('Access-Control-Allow-Origin', 'b'), headers) - self.assertNotIn(('Access-Control-Allow-Origin', '*'), headers) + assert ('Access-Control-Allow-Origin', 'b') not in headers + assert ('Access-Control-Allow-Origin', '*') not in headers def test_connect_cors_headers_default_origin(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'foo', - 'HTTP_ORIGIN': 'http://foo'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'wsgi.url_scheme': 'http', + 'HTTP_HOST': 'foo', + 'HTTP_ORIGIN': 'http://foo', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'http://foo'), - headers) + assert ('Access-Control-Allow-Origin', 'http://foo') in headers def test_connect_cors_headers_default_origin_proxy_server(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'foo', - 'HTTP_ORIGIN': 'https://bar', - 'HTTP_X_FORWARDED_PROTO': 'https, ftp', - 'HTTP_X_FORWARDED_HOST': 'bar , baz'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'wsgi.url_scheme': 'http', + 'HTTP_HOST': 'foo', + 'HTTP_ORIGIN': 'https://bar', + 'HTTP_X_FORWARDED_PROTO': 'https, ftp', + 'HTTP_X_FORWARDED_HOST': 'bar , baz', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Origin', 'https://bar'), - headers) + assert ('Access-Control-Allow-Origin', 'https://bar') in headers def test_connect_cors_no_credentials(self): s = server.Server(cors_credentials=False) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertNotIn(('Access-Control-Allow-Credentials', 'true'), headers) + assert ('Access-Control-Allow-Credentials', 'true') not in headers def test_cors_options(self): s = server.Server() - environ = {'REQUEST_METHOD': 'OPTIONS', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'OPTIONS', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Methods', 'OPTIONS, GET, POST'), - headers) + assert ( + 'Access-Control-Allow-Methods', + 'OPTIONS, GET, POST', + ) in headers def test_cors_request_headers(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', - 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS': 'Foo, Bar'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS': 'Foo, Bar', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] - self.assertIn(('Access-Control-Allow-Headers', 'Foo, Bar'), headers) + assert ('Access-Control-Allow-Headers', 'Foo, Bar') in headers def test_connect_cors_disabled(self): s = server.Server(cors_allowed_origins=[]) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', - 'HTTP_ORIGIN': 'http://foo'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4', + 'HTTP_ORIGIN': 'http://foo', + } start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' headers = start_response.call_args[0][1] for header in headers: - self.assertFalse(header[0].startswith('Access-Control-')) + assert not header[0].startswith('Access-Control-') def test_connect_cors_default_no_origin(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) headers = start_response.call_args[0][1] for header in headers: - self.assertNotEqual(header[0], 'Access-Control-Allow-Origin') + assert header[0] != 'Access-Control-Allow-Origin' def test_connect_cors_all_no_origin(self): s = server.Server(cors_allowed_origins='*') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) headers = start_response.call_args[0][1] for header in headers: - self.assertNotEqual(header[0], 'Access-Control-Allow-Origin') + assert header[0] != 'Access-Control-Allow-Origin' def test_connect_cors_disabled_no_origin(self): s = server.Server(cors_allowed_origins=[]) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) headers = start_response.call_args[0][1] for header in headers: - self.assertNotEqual(header[0], 'Access-Control-Allow-Origin') + assert header[0] != 'Access-Control-Allow-Origin' def test_connect_event(self): s = server.Server() - s._generate_id = mock.MagicMock(return_value='123') - mock_event = mock.MagicMock() + s.generate_id = mock.MagicMock(return_value='123') + mock_event = mock.MagicMock(return_value=None) s.on('connect')(mock_event) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) mock_event.assert_called_once_with('123', environ) - self.assertEqual(len(s.sockets), 1) + assert len(s.sockets) == 1 def test_connect_event_rejects(self): s = server.Server() - s._generate_id = mock.MagicMock(return_value='123') + s.generate_id = mock.MagicMock(return_value='123') mock_event = mock.MagicMock(return_value=False) s.on('connect')(mock_event) - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() - s.handle_request(environ, start_response) - self.assertEqual(len(s.sockets), 0) - self.assertEqual(start_response.call_args[0][0], '401 UNAUTHORIZED') + ret = s.handle_request(environ, start_response) + assert len(s.sockets) == 0 + assert start_response.call_args[0][0] == '401 UNAUTHORIZED' + assert ret == [b'"Unauthorized"'] + + def test_connect_event_rejects_with_message(self): + s = server.Server() + s.generate_id = mock.MagicMock(return_value='123') + mock_event = mock.MagicMock(return_value='not allowed') + s.on('connect')(mock_event) + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} + start_response = mock.MagicMock() + ret = s.handle_request(environ, start_response) + assert len(s.sockets) == 0 + assert start_response.call_args[0][0] == '401 UNAUTHORIZED' + assert ret == [b'"not allowed"'] def test_method_not_found(self): s = server.Server() - environ = {'REQUEST_METHOD': 'PUT', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'PUT', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '405 METHOD NOT FOUND') + assert start_response.call_args[0][0] == '405 METHOD NOT FOUND' def test_get_request_with_bad_sid(self): s = server.Server() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') + assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_post_request_with_bad_sid(self): s = server.Server() - environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') + assert start_response.call_args[0][0] == '400 BAD REQUEST' def test_send(self): s = server.Server() mock_socket = self._get_mock_socket() s.sockets['foo'] = mock_socket s.send('foo', 'hello') - self.assertEqual(mock_socket.send.call_count, 1) - self.assertEqual(mock_socket.send.call_args[0][0].packet_type, - packet.MESSAGE) - self.assertEqual(mock_socket.send.call_args[0][0].data, 'hello') + assert mock_socket.send.call_count == 1 + assert mock_socket.send.call_args[0][0].packet_type == packet.MESSAGE + assert mock_socket.send.call_args[0][0].data == 'hello' def test_send_unknown_socket(self): s = server.Server() @@ -785,27 +810,27 @@ class TestServer(unittest.TestCase): def test_get_request(self): s = server.Server() mock_socket = self._get_mock_socket() - mock_socket.handle_get_request = mock.MagicMock(return_value=[ - packet.Packet(packet.MESSAGE, data='hello')]) + mock_socket.handle_get_request = mock.MagicMock( + return_value=[packet.Packet(packet.MESSAGE, data='hello')] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() r = s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') - self.assertEqual(len(r), 1) - packets = payload.Payload(encoded_payload=r[0]).packets - self.assertEqual(len(packets), 1) - self.assertEqual(packets[0].packet_type, packet.MESSAGE) + assert start_response.call_args[0][0] == '200 OK' + assert len(r) == 1 + packets = payload.Payload(encoded_payload=r[0].decode('utf-8')).packets + assert len(packets) == 1 + assert packets[0].packet_type == packet.MESSAGE def test_get_request_custom_response(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock(side_effect=['resp']) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() - self.assertEqual(s.handle_request(environ, start_response), 'resp') + assert s.handle_request(environ, start_response) == 'resp' def test_get_request_closes_socket(self): s = server.Server() @@ -817,169 +842,214 @@ class TestServer(unittest.TestCase): mock_socket.handle_get_request = mock_get_request s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() - self.assertEqual(s.handle_request(environ, start_response), 'resp') - self.assertNotIn('foo', s.sockets) + assert s.handle_request(environ, start_response) == 'resp' + assert 'foo' not in s.sockets def test_get_request_error(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_get_request = mock.MagicMock( - side_effect=[exceptions.QueueEmpty]) + side_effect=[exceptions.QueueEmpty] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') - self.assertEqual(len(s.sockets), 0) + assert start_response.call_args[0][0] == '400 BAD REQUEST' + assert len(s.sockets) == 0 def test_post_request(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_post_request = mock.MagicMock() s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '200 OK') + assert start_response.call_args[0][0] == '200 OK' def test_post_request_error(self): s = server.Server() mock_socket = self._get_mock_socket() mock_socket.handle_post_request = mock.MagicMock( - side_effect=[exceptions.EngineIOError]) + side_effect=[exceptions.EngineIOError] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo'} + environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'EIO=4&sid=foo'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertEqual(start_response.call_args[0][0], - '400 BAD REQUEST') - self.assertNotIn('foo', s.sockets) + assert start_response.call_args[0][0] == '400 BAD REQUEST' + assert 'foo' not in s.sockets @staticmethod def _gzip_decompress(b): - bytesio = six.BytesIO(b) + bytesio = io.BytesIO(b) with gzip.GzipFile(fileobj=bytesio, mode='r') as gz: return gz.read() def test_gzip_compression(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() - mock_socket.handle_get_request = mock.MagicMock(return_value=[ - packet.Packet(packet.MESSAGE, data='hello')]) + mock_socket.handle_get_request = mock.MagicMock( + return_value=[packet.Packet(packet.MESSAGE, data='hello')] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'gzip,deflate'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&sid=foo', + 'HTTP_ACCEPT_ENCODING': 'gzip,deflate', + } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) - self.assertIn(('Content-Encoding', 'gzip'), - start_response.call_args[0][1]) + assert ('Content-Encoding', 'gzip') in start_response.call_args[0][1] self._gzip_decompress(r[0]) def test_deflate_compression(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() - mock_socket.handle_get_request = mock.MagicMock(return_value=[ - packet.Packet(packet.MESSAGE, data='hello')]) + mock_socket.handle_get_request = mock.MagicMock( + return_value=[packet.Packet(packet.MESSAGE, data='hello')] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'deflate;q=1,gzip'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&sid=foo', + 'HTTP_ACCEPT_ENCODING': 'deflate;q=1,gzip', + } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) - self.assertIn(('Content-Encoding', 'deflate'), - start_response.call_args[0][1]) + assert ('Content-Encoding', 'deflate') in start_response.call_args[0][ + 1 + ] zlib.decompress(r[0]) def test_gzip_compression_threshold(self): s = server.Server(compression_threshold=1000) mock_socket = self._get_mock_socket() - mock_socket.handle_get_request = mock.MagicMock(return_value=[ - packet.Packet(packet.MESSAGE, data='hello')]) + mock_socket.handle_get_request = mock.MagicMock( + return_value=[packet.Packet(packet.MESSAGE, data='hello')] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'gzip'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&sid=foo', + 'HTTP_ACCEPT_ENCODING': 'gzip', + } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, r[0]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(r[0]) def test_compression_disabled(self): s = server.Server(http_compression=False, compression_threshold=0) mock_socket = self._get_mock_socket() - mock_socket.handle_get_request = mock.MagicMock(return_value=[ - packet.Packet(packet.MESSAGE, data='hello')]) + mock_socket.handle_get_request = mock.MagicMock( + return_value=[packet.Packet(packet.MESSAGE, data='hello')] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'gzip'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&sid=foo', + 'HTTP_ACCEPT_ENCODING': 'gzip', + } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, r[0]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(r[0]) def test_compression_unknown(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() - mock_socket.handle_get_request = mock.MagicMock(return_value=[ - packet.Packet(packet.MESSAGE, data='hello')]) + mock_socket.handle_get_request = mock.MagicMock( + return_value=[packet.Packet(packet.MESSAGE, data='hello')] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': 'rar'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&sid=foo', + 'HTTP_ACCEPT_ENCODING': 'rar', + } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, r[0]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(r[0]) def test_compression_no_encoding(self): s = server.Server(compression_threshold=0) mock_socket = self._get_mock_socket() - mock_socket.handle_get_request = mock.MagicMock(return_value=[ - packet.Packet(packet.MESSAGE, data='hello')]) + mock_socket.handle_get_request = mock.MagicMock( + return_value=[packet.Packet(packet.MESSAGE, data='hello')] + ) s.sockets['foo'] = mock_socket - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_ACCEPT_ENCODING': ''} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'EIO=4&sid=foo', + 'HTTP_ACCEPT_ENCODING': '', + } start_response = mock.MagicMock() r = s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: - self.assertNotEqual(header, 'Content-Encoding') - self.assertRaises(IOError, self._gzip_decompress, r[0]) + assert header != 'Content-Encoding' + with pytest.raises(IOError): + self._gzip_decompress(r[0]) def test_cookie(self): s = server.Server(cookie='sid') - s._generate_id = mock.MagicMock(return_value='123') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + s.generate_id = mock.MagicMock(return_value='123') + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) - self.assertIn(('Set-Cookie', 'sid=123'), - start_response.call_args[0][1]) + assert ('Set-Cookie', 'sid=123; path=/; SameSite=Lax') \ + in start_response.call_args[0][1] + + def test_cookie_dict(self): + def get_path(): + return '/a' + + s = server.Server(cookie={ + 'name': 'test', + 'path': get_path, + 'SameSite': 'None', + 'Secure': True, + 'HttpOnly': True + }) + s.generate_id = mock.MagicMock(return_value='123') + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} + start_response = mock.MagicMock() + s.handle_request(environ, start_response) + assert ('Set-Cookie', 'test=123; path=/a; SameSite=None; Secure; ' + 'HttpOnly') in start_response.call_args[0][1] def test_no_cookie(self): s = server.Server(cookie=None) - s._generate_id = mock.MagicMock(return_value='123') - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + s.generate_id = mock.MagicMock(return_value='123') + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) for header, value in start_response.call_args[0][1]: - self.assertNotEqual(header, 'Set-Cookie') + assert header != 'Set-Cookie' def test_logger(self): s = server.Server(logger=False) - self.assertEqual(s.logger.getEffectiveLevel(), logging.ERROR) + assert s.logger.getEffectiveLevel() == logging.ERROR s.logger.setLevel(logging.NOTSET) s = server.Server(logger=True) - self.assertEqual(s.logger.getEffectiveLevel(), logging.INFO) + assert s.logger.getEffectiveLevel() == logging.INFO s.logger.setLevel(logging.WARNING) s = server.Server(logger=True) - self.assertEqual(s.logger.getEffectiveLevel(), logging.WARNING) + assert s.logger.getEffectiveLevel() == logging.WARNING s.logger.setLevel(logging.NOTSET) my_logger = logging.Logger('foo') s = server.Server(logger=my_logger) - self.assertEqual(s.logger, my_logger) + assert s.logger == my_logger def test_custom_json(self): # Warning: this test cannot run in parallel with other tests, as it @@ -996,9 +1066,9 @@ class TestServer(unittest.TestCase): server.Server(json=CustomJSON) pkt = packet.Packet(packet.MESSAGE, data={'foo': 'bar'}) - self.assertEqual(pkt.encode(), b'4*** encoded ***') + assert pkt.encode() == '4*** encoded ***' pkt2 = packet.Packet(encoded_packet=pkt.encode()) - self.assertEqual(pkt2.data, '+++ decoded +++') + assert pkt2.data == '+++ decoded +++' # restore the default JSON module packet.Packet.json = json @@ -1012,32 +1082,42 @@ class TestServer(unittest.TestCase): s = server.Server() task = s.start_background_task(bg_task) task.join() - self.assertIn('task', flag) - self.assertTrue(flag['task']) + assert 'task' in flag + assert flag['task'] def test_sleep(self): s = server.Server() t = time.time() s.sleep(0.1) - self.assertTrue(time.time() - t > 0.1) + assert time.time() - t > 0.1 def test_create_queue(self): s = server.Server() q = s.create_queue() empty = s.get_queue_empty_exception() - self.assertRaises(empty, q.get, timeout=0.01) + with pytest.raises(empty): + q.get(timeout=0.01) def test_create_event(self): s = server.Server() e = s.create_event() - self.assertFalse(e.is_set()) + assert not e.is_set() e.set() - self.assertTrue(e.is_set()) + assert e.is_set() + + def test_log_error_once(self): + s = server.Server(logger=mock.MagicMock()) + s._log_error_once('foo', 'foo-key') + s._log_error_once('foo', 'foo-key') + s.logger.error.assert_called_with( + 'foo (further occurrences of this error will be logged with ' + 'level INFO)') + s.logger.info.assert_called_with('foo') def test_service_task_started(self): s = server.Server(monitor_clients=True) s._service_task = mock.MagicMock() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': ''} + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'EIO=4'} start_response = mock.MagicMock() s.handle_request(environ, start_response) s._service_task.assert_called_once_with() diff --git a/debian/tests/common/test_socket.py b/debian/tests/common/test_socket.py index 543ff6d..33785e9 100644 --- a/debian/tests/common/test_socket.py +++ b/debian/tests/common/test_socket.py @@ -1,11 +1,9 @@ +import io import time import unittest +from unittest import mock -import six -if six.PY3: - from unittest import mock -else: - import mock +import pytest from engineio import exceptions from engineio import packet @@ -29,9 +27,12 @@ class TestSocket(unittest.TestCase): except ImportError: import Queue as queue import threading - mock_server._async = {'threading': threading.Thread, - 'queue': queue.Queue, - 'websocket': None} + + mock_server._async = { + 'threading': threading.Thread, + 'queue': queue.Queue, + 'websocket': None, + } def bg_task(target, *args, **kwargs): th = threading.Thread(target=target, args=args, kwargs=kwargs) @@ -54,19 +55,20 @@ class TestSocket(unittest.TestCase): def test_create(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') - self.assertEqual(s.server, mock_server) - self.assertEqual(s.sid, 'sid') - self.assertFalse(s.upgraded) - self.assertFalse(s.closed) - self.assertTrue(hasattr(s.queue, 'get')) - self.assertTrue(hasattr(s.queue, 'put')) - self.assertTrue(hasattr(s.queue, 'task_done')) - self.assertTrue(hasattr(s.queue, 'join')) + assert s.server == mock_server + assert s.sid == 'sid' + assert not s.upgraded + assert not s.closed + assert hasattr(s.queue, 'get') + assert hasattr(s.queue, 'put') + assert hasattr(s.queue, 'task_done') + assert hasattr(s.queue, 'join') def test_empty_poll(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') - self.assertRaises(exceptions.QueueEmpty, s.poll) + with pytest.raises(exceptions.QueueEmpty): + s.poll() def test_poll(self): mock_server = self._get_mock_server() @@ -75,38 +77,73 @@ class TestSocket(unittest.TestCase): pkt2 = packet.Packet(packet.MESSAGE, data='bye') s.send(pkt1) s.send(pkt2) - self.assertEqual(s.poll(), [pkt1, pkt2]) + assert s.poll() == [pkt1, pkt2] - def test_ping_pong(self): + def test_poll_none(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') - s.receive(packet.Packet(packet.PING, data='abc')) - r = s.poll() - self.assertEqual(len(r), 1) - self.assertTrue(r[0].encode(), b'3abc') + s.queue.put(None) + assert s.poll() == [] + + def test_poll_none_after_packet(self): + mock_server = self._get_mock_server() + s = socket.Socket(mock_server, 'sid') + pkt = packet.Packet(packet.MESSAGE, data='hello') + s.send(pkt) + s.queue.put(None) + assert s.poll() == [pkt] + assert s.poll() == [] + + def test_schedule_ping(self): + mock_server = self._get_mock_server() + mock_server.ping_interval = 0.01 + s = socket.Socket(mock_server, 'sid') + s.send = mock.MagicMock() + s.schedule_ping() + time.sleep(0.05) + assert s.last_ping is not None + assert s.send.call_args_list[0][0][0].encode() == '2' + + def test_schedule_ping_closed_socket(self): + mock_server = self._get_mock_server() + mock_server.ping_interval = 0.01 + s = socket.Socket(mock_server, 'sid') + s.send = mock.MagicMock() + s.closed = True + s.schedule_ping() + time.sleep(0.05) + assert s.last_ping is None + s.send.assert_not_called() + + def test_pong(self): + mock_server = self._get_mock_server() + s = socket.Socket(mock_server, 'sid') + s.schedule_ping = mock.MagicMock() + s.receive(packet.Packet(packet.PONG)) + s.schedule_ping.assert_called_once_with() def test_message_async_handler(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.receive(packet.Packet(packet.MESSAGE, data='foo')) - mock_server._trigger_event.assert_called_once_with('message', 'sid', - 'foo', - run_async=True) + mock_server._trigger_event.assert_called_once_with( + 'message', 'sid', 'foo', run_async=True + ) def test_message_sync_handler(self): mock_server = self._get_mock_server() mock_server.async_handlers = False s = socket.Socket(mock_server, 'sid') s.receive(packet.Packet(packet.MESSAGE, data='foo')) - mock_server._trigger_event.assert_called_once_with('message', 'sid', - 'foo', - run_async=False) + mock_server._trigger_event.assert_called_once_with( + 'message', 'sid', 'foo', run_async=False + ) def test_invalid_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') - self.assertRaises(exceptions.UnknownPacketError, s.receive, - packet.Packet(packet.OPEN)) + with pytest.raises(exceptions.UnknownPacketError): + s.receive(packet.Packet(packet.OPEN)) def test_timeout(self): mock_server = self._get_mock_server() @@ -128,49 +165,60 @@ class TestSocket(unittest.TestCase): environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} start_response = mock.MagicMock() packets = s.handle_get_request(environ, start_response) - self.assertEqual(packets, [pkt1, pkt2]) + assert packets == [pkt1, pkt2] def test_polling_read_error(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'foo') environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo'} start_response = mock.MagicMock() - self.assertRaises(exceptions.QueueEmpty, s.handle_get_request, environ, - start_response) + with pytest.raises(exceptions.QueueEmpty): + s.handle_get_request(environ, start_response) def test_polling_write(self): mock_server = self._get_mock_server() mock_server.max_http_buffer_size = 1000 pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') - p = payload.Payload(packets=[pkt1, pkt2]).encode() + p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') s = socket.Socket(mock_server, 'foo') s.receive = mock.MagicMock() - environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', - 'CONTENT_LENGTH': len(p), 'wsgi.input': six.BytesIO(p)} + environ = { + 'REQUEST_METHOD': 'POST', + 'QUERY_STRING': 'sid=foo', + 'CONTENT_LENGTH': len(p), + 'wsgi.input': io.BytesIO(p), + } s.handle_post_request(environ) - self.assertEqual(s.receive.call_count, 2) + assert s.receive.call_count == 2 def test_polling_write_too_large(self): mock_server = self._get_mock_server() pkt1 = packet.Packet(packet.MESSAGE, data='hello') pkt2 = packet.Packet(packet.MESSAGE, data='bye') - p = payload.Payload(packets=[pkt1, pkt2]).encode() + p = payload.Payload(packets=[pkt1, pkt2]).encode().encode('utf-8') mock_server.max_http_buffer_size = len(p) - 1 s = socket.Socket(mock_server, 'foo') s.receive = mock.MagicMock() - environ = {'REQUEST_METHOD': 'POST', 'QUERY_STRING': 'sid=foo', - 'CONTENT_LENGTH': len(p), 'wsgi.input': six.BytesIO(p)} - self.assertRaises(exceptions.ContentTooLongError, - s.handle_post_request, environ) + environ = { + 'REQUEST_METHOD': 'POST', + 'QUERY_STRING': 'sid=foo', + 'CONTENT_LENGTH': len(p), + 'wsgi.input': io.BytesIO(p), + } + with pytest.raises(exceptions.ContentTooLongError): + s.handle_post_request(environ) def test_upgrade_handshake(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'foo') s._upgrade_websocket = mock.MagicMock() - environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=foo', - 'HTTP_CONNECTION': 'Foo,Upgrade,Bar', - 'HTTP_UPGRADE': 'websocket'} + environ = { + 'REQUEST_METHOD': 'GET', + 'QUERY_STRING': 'sid=foo', + 'HTTP_CONNECTION': 'Foo,Upgrade,Bar', + 'HTTP_UPGRADE': 'websocket', + } start_response = mock.MagicMock() s.handle_get_request(environ, start_response) s._upgrade_websocket.assert_called_once_with(environ, start_response) @@ -186,7 +234,8 @@ class TestSocket(unittest.TestCase): start_response = "bar" s._upgrade_websocket(environ, start_response) mock_server._async['websocket'].assert_called_once_with( - s._websocket_handler) + s._websocket_handler + ) mock_ws.assert_called_once_with(environ, start_response) def test_upgrade_twice(self): @@ -197,8 +246,8 @@ class TestSocket(unittest.TestCase): s.upgraded = True environ = "foo" start_response = "bar" - self.assertRaises(IOError, s._upgrade_websocket, - environ, start_response) + with pytest.raises(IOError): + s._upgrade_websocket(environ, start_response) def test_upgrade_packet(self): mock_server = self._get_mock_server() @@ -206,18 +255,17 @@ class TestSocket(unittest.TestCase): s.connected = True s.receive(packet.Packet(packet.UPGRADE)) r = s.poll() - self.assertEqual(len(r), 1) - self.assertEqual(r[0].encode(), packet.Packet(packet.NOOP).encode()) + assert len(r) == 1 + assert r[0].encode() == packet.Packet(packet.NOOP).encode() def test_upgrade_no_probe(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True ws = mock.MagicMock() - ws.wait.return_value = packet.Packet(packet.NOOP).encode( - always_bytes=False) + ws.wait.return_value = packet.Packet(packet.NOOP).encode() s._websocket_handler(ws) - self.assertFalse(s.upgraded) + assert not s.upgraded def test_upgrade_no_upgrade_packet(self): mock_server = self._get_mock_server() @@ -225,16 +273,17 @@ class TestSocket(unittest.TestCase): s.connected = True s.queue.join = mock.MagicMock(return_value=None) ws = mock.MagicMock() - probe = six.text_type('probe') + probe = 'probe' ws.wait.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.NOOP).encode(always_bytes=False)] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.NOOP).encode(), + ] s._websocket_handler(ws) - ws.send.assert_called_once_with(packet.Packet( - packet.PONG, data=probe).encode(always_bytes=False)) - self.assertEqual(s.queue.get().packet_type, packet.NOOP) - self.assertFalse(s.upgraded) + ws.send.assert_called_once_with( + packet.Packet(packet.PONG, data=probe).encode() + ) + assert s.queue.get().packet_type == packet.NOOP + assert not s.upgraded def test_close_packet(self): mock_server = self._get_mock_server() @@ -248,7 +297,8 @@ class TestSocket(unittest.TestCase): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') pkt = packet.Packet(packet_type=99) - self.assertRaises(exceptions.UnknownPacketError, s.receive, pkt) + with pytest.raises(exceptions.UnknownPacketError): + s.receive(pkt) def test_upgrade_not_supported(self): mock_server = self._get_mock_server() @@ -265,23 +315,30 @@ class TestSocket(unittest.TestCase): s = socket.Socket(mock_server, 'sid') s.connected = False s.queue.join = mock.MagicMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - s.poll = mock.MagicMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty]) + foo = 'foo' + bar = 'bar' + s.poll = mock.MagicMock( + side_effect=[ + [packet.Packet(packet.MESSAGE, data=bar)], + exceptions.QueueEmpty, + ] + ) ws = mock.MagicMock() ws.wait.side_effect = [ - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - None] + packet.Packet(packet.MESSAGE, data=foo).encode(), + None, + ] s._websocket_handler(ws) self._join_bg_tasks() - self.assertTrue(s.connected) - self.assertTrue(s.upgraded) - self.assertEqual(mock_server._trigger_event.call_count, 2) - mock_server._trigger_event.assert_has_calls([ - mock.call('message', 'sid', 'foo', run_async=True), - mock.call('disconnect', 'sid', run_async=False)]) + assert s.connected + assert s.upgraded + assert mock_server._trigger_event.call_count == 2 + mock_server._trigger_event.assert_has_calls( + [ + mock.call('message', 'sid', 'foo', run_async=True), + mock.call('disconnect', 'sid', run_async=False), + ] + ) ws.send.assert_called_with('4bar') def test_websocket_upgrade_read_write(self): @@ -289,26 +346,32 @@ class TestSocket(unittest.TestCase): s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - probe = six.text_type('probe') - s.poll = mock.MagicMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty]) + foo = 'foo' + bar = 'bar' + probe = 'probe' + s.poll = mock.MagicMock( + side_effect=[ + [packet.Packet(packet.MESSAGE, data=bar)], + exceptions.QueueEmpty, + ] + ) ws = mock.MagicMock() ws.wait.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.UPGRADE).encode(always_bytes=False), - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - None] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.UPGRADE).encode(), + packet.Packet(packet.MESSAGE, data=foo).encode(), + None, + ] s._websocket_handler(ws) self._join_bg_tasks() - self.assertTrue(s.upgraded) - self.assertEqual(mock_server._trigger_event.call_count, 2) - mock_server._trigger_event.assert_has_calls([ - mock.call('message', 'sid', 'foo', run_async=True), - mock.call('disconnect', 'sid', run_async=False)]) + assert s.upgraded + assert mock_server._trigger_event.call_count == 2 + mock_server._trigger_event.assert_has_calls( + [ + mock.call('message', 'sid', 'foo', run_async=True), + mock.call('disconnect', 'sid', run_async=False), + ] + ) ws.send.assert_called_with('4bar') def test_websocket_upgrade_with_payload(self): @@ -316,100 +379,120 @@ class TestSocket(unittest.TestCase): s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) - probe = six.text_type('probe') + probe = 'probe' ws = mock.MagicMock() ws.wait.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.UPGRADE, data=b'2').encode( - always_bytes=False)] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.UPGRADE, data='2').encode(), + ] s._websocket_handler(ws) self._join_bg_tasks() - self.assertTrue(s.upgraded) + assert s.upgraded def test_websocket_upgrade_with_backlog(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = True s.queue.join = mock.MagicMock(return_value=None) - probe = six.text_type('probe') - foo = six.text_type('foo') + probe = 'probe' + foo = 'foo' ws = mock.MagicMock() ws.wait.side_effect = [ - packet.Packet(packet.PING, data=probe).encode( - always_bytes=False), - packet.Packet(packet.UPGRADE, data=b'2').encode( - always_bytes=False)] + packet.Packet(packet.PING, data=probe).encode(), + packet.Packet(packet.UPGRADE, data='2').encode(), + ] s.upgrading = True s.send(packet.Packet(packet.MESSAGE, data=foo)) + environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': 'sid=sid'} + start_response = mock.MagicMock() + packets = s.handle_get_request(environ, start_response) + assert len(packets) == 1 + assert packets[0].encode() == '6' + packets = s.poll() + assert len(packets) == 1 + assert packets[0].encode() == '4foo' + s._websocket_handler(ws) self._join_bg_tasks() - self.assertTrue(s.upgraded) - self.assertFalse(s.upgrading) - self.assertEqual(s.packet_backlog, []) - ws.send.assert_called_with('4foo') + assert s.upgraded + assert not s.upgrading + packets = s.handle_get_request(environ, start_response) + assert len(packets) == 1 + assert packets[0].encode() == '6' def test_websocket_read_write_wait_fail(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = False s.queue.join = mock.MagicMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - s.poll = mock.MagicMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], - [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty]) + foo = 'foo' + bar = 'bar' + s.poll = mock.MagicMock( + side_effect=[ + [packet.Packet(packet.MESSAGE, data=bar)], + [packet.Packet(packet.MESSAGE, data=bar)], + exceptions.QueueEmpty, + ] + ) ws = mock.MagicMock() ws.wait.side_effect = [ - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - RuntimeError] + packet.Packet(packet.MESSAGE, data=foo).encode(), + RuntimeError, + ] ws.send.side_effect = [None, RuntimeError] s._websocket_handler(ws) self._join_bg_tasks() - self.assertEqual(s.closed, True) + assert s.closed def test_websocket_ignore_invalid_packet(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.connected = False s.queue.join = mock.MagicMock(return_value=None) - foo = six.text_type('foo') - bar = six.text_type('bar') - s.poll = mock.MagicMock(side_effect=[ - [packet.Packet(packet.MESSAGE, data=bar)], exceptions.QueueEmpty]) + foo = 'foo' + bar = 'bar' + s.poll = mock.MagicMock( + side_effect=[ + [packet.Packet(packet.MESSAGE, data=bar)], + exceptions.QueueEmpty, + ] + ) ws = mock.MagicMock() ws.wait.side_effect = [ - packet.Packet(packet.OPEN).encode(always_bytes=False), - packet.Packet(packet.MESSAGE, data=foo).encode( - always_bytes=False), - None] + packet.Packet(packet.OPEN).encode(), + packet.Packet(packet.MESSAGE, data=foo).encode(), + None, + ] s._websocket_handler(ws) self._join_bg_tasks() - self.assertTrue(s.connected) - self.assertEqual(mock_server._trigger_event.call_count, 2) - mock_server._trigger_event.assert_has_calls([ - mock.call('message', 'sid', foo, run_async=True), - mock.call('disconnect', 'sid', run_async=False)]) + assert s.connected + assert mock_server._trigger_event.call_count == 2 + mock_server._trigger_event.assert_has_calls( + [ + mock.call('message', 'sid', foo, run_async=True), + mock.call('disconnect', 'sid', run_async=False), + ] + ) ws.send.assert_called_with('4bar') def test_send_after_close(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.close(wait=False) - self.assertRaises(exceptions.SocketIsClosedError, s.send, - packet.Packet(packet.NOOP)) + with pytest.raises(exceptions.SocketIsClosedError): + s.send(packet.Packet(packet.NOOP)) def test_close_after_close(self): mock_server = self._get_mock_server() s = socket.Socket(mock_server, 'sid') s.close(wait=False) - self.assertTrue(s.closed) - self.assertEqual(mock_server._trigger_event.call_count, 1) - mock_server._trigger_event.assert_called_once_with('disconnect', 'sid', - run_async=False) + assert s.closed + assert mock_server._trigger_event.call_count == 1 + mock_server._trigger_event.assert_called_once_with( + 'disconnect', 'sid', run_async=False + ) s.close() - self.assertEqual(mock_server._trigger_event.call_count, 1) + assert mock_server._trigger_event.call_count == 1 def test_close_and_wait(self): mock_server = self._get_mock_server() @@ -423,4 +506,4 @@ class TestSocket(unittest.TestCase): s = socket.Socket(mock_server, 'sid') s.queue = mock.MagicMock() s.close(wait=False) - self.assertEqual(s.queue.join.call_count, 0) + assert s.queue.join.call_count == 0 diff --git a/engineio/__init__.py b/engineio/__init__.py index 76b5574..eb9f5fe 100644 --- a/engineio/__init__.py +++ b/engineio/__init__.py @@ -17,7 +17,7 @@ else: # pragma: no cover get_tornado_handler = None ASGIApp = None -__version__ = '3.11.1' +__version__ = '4.0.0' __all__ = ['__version__', 'Server', 'WSGIApp', 'Middleware', 'Client'] if AsyncServer is not None: # pragma: no cover diff --git a/engineio/async_drivers/aiohttp.py b/engineio/async_drivers/aiohttp.py index ad69876..a591995 100644 --- a/engineio/async_drivers/aiohttp.py +++ b/engineio/async_drivers/aiohttp.py @@ -3,7 +3,6 @@ import sys from urllib.parse import urlsplit from aiohttp.web import Response, WebSocketResponse -import six def create_route(app, engineio_server, engineio_endpoint): @@ -113,8 +112,8 @@ class WebSocket(object): # pragma: no cover async def wait(self): msg = await self._sock.receive() - if not isinstance(msg.data, six.binary_type) and \ - not isinstance(msg.data, six.text_type): + if not isinstance(msg.data, bytes) and \ + not isinstance(msg.data, str): raise IOError() return msg.data diff --git a/engineio/async_drivers/asgi.py b/engineio/async_drivers/asgi.py index 9f14ef0..fe94e59 100644 --- a/engineio/async_drivers/asgi.py +++ b/engineio/async_drivers/asgi.py @@ -1,5 +1,6 @@ import os import sys +import asyncio from engineio.static_files import get_static_file @@ -19,6 +20,10 @@ class ASGIApp: :param engineio_path: The endpoint where the Engine.IO application should be installed. The default value is appropriate for most cases. + :param on_startup: function to be called on application startup; can be + coroutine + :param on_shutdown: function to be called on application shutdown; can be + coroutine Example usage:: @@ -34,11 +39,14 @@ class ASGIApp: uvicorn.run(app, '127.0.0.1', 5000) """ def __init__(self, engineio_server, other_asgi_app=None, - static_files=None, engineio_path='engine.io'): + static_files=None, engineio_path='engine.io', + on_startup=None, on_shutdown=None): self.engineio_server = engineio_server self.other_asgi_app = other_asgi_app self.engineio_path = engineio_path.strip('/') self.static_files = static_files or {} + self.on_startup = on_startup + self.on_shutdown = on_shutdown async def __call__(self, scope, receive, send): if scope['type'] in ['http', 'websocket'] and \ @@ -73,11 +81,29 @@ class ASGIApp: await self.not_found(receive, send) async def lifespan(self, receive, send): - event = await receive() - if event['type'] == 'lifespan.startup': - await send({'type': 'lifespan.startup.complete'}) - elif event['type'] == 'lifespan.shutdown': - await send({'type': 'lifespan.shutdown.complete'}) + while True: + event = await receive() + if event['type'] == 'lifespan.startup': + if self.on_startup: + try: + await self.on_startup() \ + if asyncio.iscoroutinefunction(self.on_startup) \ + else self.on_startup() + except: + await send({'type': 'lifespan.startup.failed'}) + return + await send({'type': 'lifespan.startup.complete'}) + elif event['type'] == 'lifespan.shutdown': + if self.on_shutdown: + try: + await self.on_shutdown() \ + if asyncio.iscoroutinefunction(self.on_shutdown) \ + else self.on_shutdown() + except: + await send({'type': 'lifespan.shutdown.failed'}) + return + await send({'type': 'lifespan.shutdown.complete'}) + return async def not_found(self, receive, send): """Return a 404 Not Found error to the client.""" @@ -111,7 +137,7 @@ async def translate_request(scope, receive, send): if event['type'] == 'http.request': payload += event.get('body') or b'' elif event['type'] == 'websocket.connect': - await send({'type': 'websocket.accept'}) + pass else: return {} @@ -139,6 +165,7 @@ async def translate_request(scope, receive, send): 'SERVER_PORT': '0', 'asgi.receive': receive, 'asgi.send': send, + 'asgi.scope': scope, } for hdr_name, hdr_value in scope['headers']: @@ -163,6 +190,14 @@ async def translate_request(scope, receive, send): async def make_response(status, headers, payload, environ): headers = [(h[0].encode('utf-8'), h[1].encode('utf-8')) for h in headers] + if 'HTTP_SEC_WEBSOCKET_VERSION' in environ: + if status.startswith('200 '): + await environ['asgi.send']({'type': 'websocket.accept', + 'headers': headers}) + else: + await environ['asgi.send']({'type': 'websocket.close'}) + return + await environ['asgi.send']({'type': 'http.response.start', 'status': int(status.split(' ')[0]), 'headers': headers}) @@ -183,6 +218,7 @@ class WebSocket(object): # pragma: no cover async def __call__(self, environ): self.asgi_receive = environ['asgi.receive'] self.asgi_send = environ['asgi.send'] + await self.asgi_send({'type': 'websocket.accept'}) await self.handler(self) async def close(self): diff --git a/engineio/async_drivers/gevent_uwsgi.py b/engineio/async_drivers/gevent_uwsgi.py index 07fa2a7..bdee812 100644 --- a/engineio/async_drivers/gevent_uwsgi.py +++ b/engineio/async_drivers/gevent_uwsgi.py @@ -1,7 +1,5 @@ from __future__ import absolute_import -import six - import gevent from gevent import queue from gevent.event import Event @@ -75,7 +73,7 @@ class uWSGIWebSocket(object): # pragma: no cover def _send(self, msg): """Transmits message either in binary or UTF-8 text mode, depending on its type.""" - if isinstance(msg, six.binary_type): + if isinstance(msg, bytes): method = uwsgi.websocket_send_binary else: method = uwsgi.websocket_send @@ -86,11 +84,11 @@ class uWSGIWebSocket(object): # pragma: no cover def _decode_received(self, msg): """Returns either bytes or str, depending on message type.""" - if not isinstance(msg, six.binary_type): + if not isinstance(msg, bytes): # already decoded - do nothing return msg # only decode from utf-8 if message is not binary data - type = six.byte2int(msg[0:1]) + type = ord(msg[0:1]) if type >= 48: # no binary return msg.decode('utf-8') # binary message, don't try to decode diff --git a/engineio/async_drivers/sanic.py b/engineio/async_drivers/sanic.py index 6929654..2400224 100644 --- a/engineio/async_drivers/sanic.py +++ b/engineio/async_drivers/sanic.py @@ -1,16 +1,15 @@ import sys from urllib.parse import urlsplit -from sanic.response import HTTPResponse -try: +try: # pragma: no cover + from sanic.response import HTTPResponse from sanic.websocket import WebSocketProtocol except ImportError: - # the installed version of sanic does not have websocket support + HTTPResponse = None WebSocketProtocol = None -import six -def create_route(app, engineio_server, engineio_endpoint): +def create_route(app, engineio_server, engineio_endpoint): # pragma: no cover """This function sets up the engine.io endpoint as a route for the application. @@ -26,7 +25,7 @@ def create_route(app, engineio_server, engineio_endpoint): pass -def translate_request(request): +def translate_request(request): # pragma: no cover """This function takes the arguments passed to the request handler and uses them to generate a WSGI compatible environ dictionary. """ @@ -89,7 +88,7 @@ def translate_request(request): return environ -def make_response(status, headers, payload, environ): +def make_response(status, headers, payload, environ): # pragma: no cover """This function generates an appropriate response object for this async mode. """ @@ -129,8 +128,8 @@ class WebSocket(object): # pragma: no cover async def wait(self): data = await self._sock.recv() - if not isinstance(data, six.binary_type) and \ - not isinstance(data, six.text_type): + if not isinstance(data, bytes) and \ + not isinstance(data, str): raise IOError() return data diff --git a/engineio/async_drivers/tornado.py b/engineio/async_drivers/tornado.py index adfe18f..eb1c4de 100644 --- a/engineio/async_drivers/tornado.py +++ b/engineio/async_drivers/tornado.py @@ -5,15 +5,13 @@ from .. import exceptions import tornado.web import tornado.websocket -import six def get_tornado_handler(engineio_server): class Handler(tornado.websocket.WebSocketHandler): # pragma: no cover def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if isinstance(engineio_server.cors_allowed_origins, - six.string_types): + if isinstance(engineio_server.cors_allowed_origins, str): if engineio_server.cors_allowed_origins == '*': self.allowed_origins = None else: @@ -170,8 +168,8 @@ class WebSocket(object): # pragma: no cover async def wait(self): msg = await self.tornado_handler.get_next_message() - if not isinstance(msg, six.binary_type) and \ - not isinstance(msg, six.text_type): + if not isinstance(msg, bytes) and \ + not isinstance(msg, str): raise IOError() return msg diff --git a/engineio/asyncio_client.py b/engineio/asyncio_client.py index 5445a9b..57691d6 100644 --- a/engineio/asyncio_client.py +++ b/engineio/asyncio_client.py @@ -1,17 +1,36 @@ import asyncio +import signal import ssl +import threading try: import aiohttp except ImportError: # pragma: no cover aiohttp = None -import six from . import client from . import exceptions from . import packet from . import payload +async_signal_handler_set = False + + +def async_signal_handler(): + """SIGINT handler. + + Disconnect all active async clients. + """ + async def _handler(): + asyncio.get_event_loop().stop() + for c in client.connected_clients[:]: + if c.is_asyncio_based(): + await c.disconnect() + else: # pragma: no cover + pass + + asyncio.ensure_future(_handler()) + class AsyncClient(client.Client): """An Engine.IO client for asyncio. @@ -22,13 +41,18 @@ class AsyncClient(client.Client): :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. The default is - ``False``. + ``False``. Note that fatal errors are logged even when + ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param request_timeout: A timeout in seconds for requests. The default is 5 seconds. + :param http_session: an initialized ``aiohttp.ClientSession`` object to be + used when sending requests to the server. Use it if + you need to add special client options such as proxy + servers, SSL certificates, etc. :param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to skip SSL certificate verification, allowing connections to servers with self signed certificates. @@ -37,7 +61,7 @@ class AsyncClient(client.Client): def is_asyncio_based(self): return True - async def connect(self, url, headers={}, transports=None, + async def connect(self, url, headers=None, transports=None, engineio_path='engine.io'): """Connect to an Engine.IO server. @@ -60,11 +84,22 @@ class AsyncClient(client.Client): eio = engineio.Client() await eio.connect('http://localhost:5000') """ + global async_signal_handler_set + if not async_signal_handler_set and \ + threading.current_thread() == threading.main_thread(): + + try: + asyncio.get_event_loop().add_signal_handler( + signal.SIGINT, async_signal_handler) + async_signal_handler_set = True + except NotImplementedError: # pragma: no cover + self.logger.warning('Signal handler is unsupported') + if self.state != 'disconnected': raise ValueError('Client is not in a disconnected state') valid_transports = ['polling', 'websocket'] if transports is not None: - if isinstance(transports, six.text_type): + if isinstance(transports, str): transports = [transports] transports = [transport for transport in transports if transport in valid_transports] @@ -73,7 +108,7 @@ class AsyncClient(client.Client): self.transports = transports or valid_transports self.queue = self.create_queue() return await getattr(self, '_connect_' + self.transports[0])( - url, headers, engineio_path) + url, headers or {}, engineio_path) async def wait(self): """Wait until the connection with the server ends. @@ -86,21 +121,16 @@ class AsyncClient(client.Client): if self.read_loop_task: await self.read_loop_task - async def send(self, data, binary=None): + async def send(self, data): """Send a message to a client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. - :param binary: ``True`` to send packet as binary, ``False`` to send - as text. If not given, unicode (Python 2) and str - (Python 3) are sent as text, and str (Python 2) and - bytes (Python 3) are sent as binary. Note: this method is a coroutine. """ - await self._send_packet(packet.Packet(packet.MESSAGE, data=data, - binary=binary)) + await self._send_packet(packet.Packet(packet.MESSAGE, data=data)) async def disconnect(self, abort=False): """Disconnect from the server. @@ -182,14 +212,20 @@ class AsyncClient(client.Client): raise exceptions.ConnectionError( 'Connection refused by the server') if r.status < 200 or r.status >= 300: + self._reset() + try: + arg = await r.json() + except aiohttp.ClientError: + arg = None raise exceptions.ConnectionError( 'Unexpected status code {} in server response'.format( - r.status)) + r.status), arg) try: - p = payload.Payload(encoded_payload=await r.read()) + p = payload.Payload(encoded_payload=(await r.read()).decode( + 'utf-8')) except ValueError: - six.raise_from(exceptions.ConnectionError( - 'Unexpected response from server'), None) + raise exceptions.ConnectionError( + 'Unexpected response from server') from None open_packet = p.packets[0] if open_packet.packet_type != packet.OPEN: raise exceptions.ConnectionError( @@ -198,8 +234,8 @@ class AsyncClient(client.Client): 'Polling connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] - self.ping_interval = open_packet.data['pingInterval'] / 1000.0 - self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0 + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'polling' self.base_url += '&sid=' + self.sid @@ -216,7 +252,6 @@ class AsyncClient(client.Client): # upgrade to websocket succeeded, we're done here return - self.ping_loop_task = self.start_background_task(self._ping_loop) self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_polling) @@ -239,6 +274,20 @@ class AsyncClient(client.Client): self.logger.info( 'Attempting WebSocket connection to ' + websocket_url) + if self.http is None or self.http.closed: # pragma: no cover + self.http = aiohttp.ClientSession() + + # extract any new cookies passed in a header so that they can also be + # sent the the WebSocket route + cookies = {} + for header, value in headers.items(): + if header.lower() == 'cookie': + cookies = dict( + [cookie.split('=', 1) for cookie in value.split('; ')]) + del headers[header] + break + self.http.cookie_jar.update_cookies(cookies) + try: if not self.ssl_verify: ssl_context = ssl.create_default_context() @@ -252,7 +301,8 @@ class AsyncClient(client.Client): websocket_url + self._get_url_timestamp(), headers=headers) except (aiohttp.client_exceptions.WSServerHandshakeError, - aiohttp.client_exceptions.ServerConnectionError): + aiohttp.client_exceptions.ServerConnectionError, + aiohttp.client_exceptions.ClientConnectionError): if upgrade: self.logger.warning( 'WebSocket upgrade failed: connection error') @@ -260,8 +310,7 @@ class AsyncClient(client.Client): else: raise exceptions.ConnectionError('Connection error') if upgrade: - p = packet.Packet(packet.PING, data='probe').encode( - always_bytes=False) + p = packet.Packet(packet.PING, data='probe').encode() try: await ws.send_str(p) except Exception as e: # pragma: no cover @@ -281,7 +330,7 @@ class AsyncClient(client.Client): self.logger.warning( 'WebSocket upgrade failed: no PONG packet') return False - p = packet.Packet(packet.UPGRADE).encode(always_bytes=False) + p = packet.Packet(packet.UPGRADE).encode() try: await ws.send_str(p) except Exception as e: # pragma: no cover @@ -304,8 +353,8 @@ class AsyncClient(client.Client): 'WebSocket connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] - self.ping_interval = open_packet.data['pingInterval'] / 1000.0 - self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0 + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'websocket' self.state = 'connected' @@ -313,7 +362,6 @@ class AsyncClient(client.Client): await self._trigger_event('connect', run_async=False) self.ws = ws - self.ping_loop_task = self.start_background_task(self._ping_loop) self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_websocket) @@ -328,8 +376,8 @@ class AsyncClient(client.Client): pkt.data if not isinstance(pkt.data, bytes) else '<binary>') if pkt.packet_type == packet.MESSAGE: await self._trigger_event('message', pkt.data, run_async=True) - elif pkt.packet_type == packet.PONG: - self.pong_received = True + elif pkt.packet_type == packet.PING: + await self._send_packet(packet.Packet(packet.PONG, pkt.data)) elif pkt.packet_type == packet.CLOSE: await self.disconnect(abort=True) elif pkt.packet_type == packet.NOOP: @@ -406,33 +454,6 @@ class AsyncClient(client.Client): return False return ret - async def _ping_loop(self): - """This background task sends a PING to the server at the requested - interval. - """ - self.pong_received = True - if self.ping_loop_event is None: - self.ping_loop_event = self.create_event() - else: - self.ping_loop_event.clear() - while self.state == 'connected': - if not self.pong_received: - self.logger.info( - 'PONG response has not been received, aborting') - if self.ws: - await self.ws.close() - await self.queue.put(None) - break - self.pong_received = False - await self._send_packet(packet.Packet(packet.PING)) - try: - await asyncio.wait_for(self.ping_loop_event.wait(), - self.ping_interval) - except (asyncio.TimeoutError, - asyncio.CancelledError): # pragma: no cover - pass - self.logger.info('Exiting ping task') - async def _read_loop_polling(self): """Read packets by polling the Engine.IO server.""" while self.state == 'connected': @@ -452,7 +473,8 @@ class AsyncClient(client.Client): await self.queue.put(None) break try: - p = payload.Payload(encoded_payload=await r.read()) + p = payload.Payload(encoded_payload=(await r.read()).decode( + 'utf-8')) except ValueError: self.logger.warning( 'Unexpected packet from server, aborting') @@ -463,10 +485,6 @@ class AsyncClient(client.Client): self.logger.info('Waiting for write loop task to end') await self.write_loop_task - self.logger.info('Waiting for ping loop task to end') - if self.ping_loop_event: # pragma: no cover - self.ping_loop_event.set() - await self.ping_loop_task if self.state == 'connected': await self._trigger_event('disconnect', run_async=False) try: @@ -481,7 +499,17 @@ class AsyncClient(client.Client): while self.state == 'connected': p = None try: - p = (await self.ws.receive()).data + p = await asyncio.wait_for( + self.ws.receive(), + timeout=self.ping_interval + self.ping_timeout) + p = p.data + if p is None: # pragma: no cover + break # the connection is broken + except asyncio.TimeoutError: + self.logger.warning( + 'Server has stopped communicating, aborting') + await self.queue.put(None) + break except aiohttp.client_exceptions.ServerDisconnectedError: self.logger.info( 'Read loop: WebSocket connection was closed, aborting') @@ -489,20 +517,21 @@ class AsyncClient(client.Client): break except Exception as e: self.logger.info( - 'Unexpected error "%s", aborting', str(e)) + 'Unexpected error receiving packet: "%s", aborting', + str(e)) + await self.queue.put(None) + break + try: + pkt = packet.Packet(encoded_packet=p) + except Exception as e: # pragma: no cover + self.logger.info( + 'Unexpected error decoding packet: "%s", aborting', str(e)) await self.queue.put(None) break - if isinstance(p, six.text_type): # pragma: no cover - p = p.encode('utf-8') - pkt = packet.Packet(encoded_packet=p) await self._receive_packet(pkt) self.logger.info('Waiting for write loop task to end') await self.write_loop_task - self.logger.info('Waiting for ping loop task to end') - if self.ping_loop_event: # pragma: no cover - self.ping_loop_event.set() - await self.ping_loop_task if self.state == 'connected': await self._trigger_event('disconnect', run_async=False) try: @@ -566,13 +595,12 @@ class AsyncClient(client.Client): try: for pkt in packets: if pkt.binary: - await self.ws.send_bytes(pkt.encode( - always_bytes=False)) + await self.ws.send_bytes(pkt.encode()) else: - await self.ws.send_str(pkt.encode( - always_bytes=False)) + await self.ws.send_str(pkt.encode()) self.queue.task_done() - except aiohttp.client_exceptions.ServerDisconnectedError: + except (aiohttp.client_exceptions.ServerDisconnectedError, + BrokenPipeError, OSError): self.logger.info( 'Write loop: WebSocket connection was closed, ' 'aborting') diff --git a/engineio/asyncio_server.py b/engineio/asyncio_server.py index d52b556..6639f26 100644 --- a/engineio/asyncio_server.py +++ b/engineio/asyncio_server.py @@ -1,7 +1,5 @@ import asyncio - -import six -from six.moves import urllib +import urllib from . import exceptions from . import packet @@ -24,23 +22,30 @@ class AsyncServer(server.Server): "tornado", and finally "asgi". The first async mode that has all its dependencies installed is the one that is chosen. - :param ping_timeout: The time in seconds that the client waits for the - server to respond before disconnecting. - :param ping_interval: The interval in seconds at which the client pings - the server. The default is 25 seconds. For advanced + :param ping_interval: The interval in seconds at which the server pings + the client. The default is 25 seconds. For advanced control, a two element tuple can be given, where the first number is the ping interval and the second - is a grace period added by the server. The default - grace period is 5 seconds. + is a grace period added by the server. + :param ping_timeout: The time in seconds that the client waits for the + server to respond before disconnecting. The default + is 5 seconds. :param max_http_buffer_size: The maximum size of a message when using the - polling transport. + polling transport. The default is 1,000,000 + bytes. :param allow_upgrades: Whether to allow transport upgrades or not. :param http_compression: Whether to compress packages when using the polling transport. :param compression_threshold: Only compress messages when their byte size is greater than this value. - :param cookie: Name of the HTTP cookie that contains the client session - id. If set to ``None``, a cookie is not sent to the client. + :param cookie: If set to a string, it is the name of the HTTP cookie the + server sends back tot he client containing the client + session id. If set to a dictionary, the ``'name'`` key + contains the cookie name and other keys define cookie + attributes, where the value of each attribute can be a + string, a callable with no arguments, or a boolean. If set + to ``None`` (the default), a cookie is not sent to the + client. :param cors_allowed_origins: Origin or list of origins that are allowed to connect to this server. Only the same origin is allowed by default. Set this argument to @@ -49,7 +54,8 @@ class AsyncServer(server.Server): :param cors_credentials: Whether credentials (cookies, authentication) are allowed in requests to this server. :param logger: To enable logging set to ``True`` or pass a logger object to - use. To disable logging set to ``False``. + use. To disable logging set to ``False``. Note that fatal + errors are logged even when ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library @@ -71,17 +77,13 @@ class AsyncServer(server.Server): engineio_path = engineio_path.strip('/') self._async['create_route'](app, self, '/{}/'.format(engineio_path)) - async def send(self, sid, data, binary=None): + async def send(self, sid, data): """Send a message to a client. :param sid: The session id of the recipient client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. - :param binary: ``True`` to send packet as binary, ``False`` to send - as text. If not given, unicode (Python 2) and str - (Python 3) are sent as text, and str (Python 2) and - bytes (Python 3) are sent as binary. Note: this method is a coroutine. """ @@ -91,8 +93,7 @@ class AsyncServer(server.Server): # the socket is not available self.logger.warning('Cannot send to sid %s', sid) return - await socket.send(packet.Packet(packet.MESSAGE, data=data, - binary=binary)) + await socket.send(packet.Packet(packet.MESSAGE, data=data)) async def get_session(self, sid): """Return the user session for a client. @@ -172,7 +173,7 @@ class AsyncServer(server.Server): del self.sockets[sid] else: await asyncio.wait([client.close() - for client in six.itervalues(self.sockets)]) + for client in self.sockets.values()]) self.sockets = {} async def handle_request(self, *args, **kwargs): @@ -198,28 +199,32 @@ class AsyncServer(server.Server): allowed_origins = self._cors_allowed_origins(environ) if allowed_origins is not None and origin not in \ allowed_origins: - self.logger.info(origin + ' is not an accepted origin.') - r = self._bad_request() - make_response = self._async['make_response'] - if asyncio.iscoroutinefunction(make_response): - response = await make_response( - r['status'], r['headers'], r['response'], environ) - else: - response = make_response(r['status'], r['headers'], - r['response'], environ) - return response + self._log_error_once( + origin + ' is not an accepted origin.', 'bad-origin') + return await self._make_response( + self._bad_request( + origin + ' is not an accepted origin.'), + environ) method = environ['REQUEST_METHOD'] query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) sid = query['sid'][0] if 'sid' in query else None - b64 = False jsonp = False jsonp_index = None - if 'b64' in query: - if query['b64'][0] == "1" or query['b64'][0].lower() == "true": - b64 = True + # make sure the client speaks a compatible Engine.IO version + sid = query['sid'][0] if 'sid' in query else None + if sid is None and query.get('EIO') != ['4']: + self._log_error_once( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols', 'bad-version' + ) + return await self._make_response(self._bad_request( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols' + ), environ) + if 'j' in query: jsonp = True try: @@ -229,28 +234,34 @@ class AsyncServer(server.Server): pass if jsonp and jsonp_index is None: - self.logger.warning('Invalid JSONP index number') - r = self._bad_request() + self._log_error_once('Invalid JSONP index number', + 'bad-jsonp-index') + r = self._bad_request('Invalid JSONP index number') elif method == 'GET': if sid is None: transport = query.get('transport', ['polling'])[0] - if transport != 'polling' and transport != 'websocket': - self.logger.warning('Invalid transport %s', transport) - r = self._bad_request() - else: + # transport must be one of 'polling' or 'websocket'. + # if 'websocket', the HTTP_UPGRADE header must match. + upgrade_header = environ.get('HTTP_UPGRADE').lower() \ + if 'HTTP_UPGRADE' in environ else None + if transport == 'polling' \ + or transport == upgrade_header == 'websocket': r = await self._handle_connect(environ, transport, - b64, jsonp_index) + jsonp_index) + else: + self._log_error_once('Invalid transport ' + transport, + 'bad-transport') + r = self._bad_request('Invalid transport ' + transport) else: if sid not in self.sockets: - self.logger.warning('Invalid session %s', sid) - r = self._bad_request() + self._log_error_once('Invalid session ' + sid, 'bad-sid') + r = self._bad_request('Invalid session ' + sid) else: socket = self._get_socket(sid) try: packets = await socket.handle_get_request(environ) if isinstance(packets, list): - r = self._ok(packets, b64=b64, - jsonp_index=jsonp_index) + r = self._ok(packets, jsonp_index=jsonp_index) else: r = packets except exceptions.EngineIOError: @@ -261,8 +272,8 @@ class AsyncServer(server.Server): del self.sockets[sid] elif method == 'POST': if sid is None or sid not in self.sockets: - self.logger.warning('Invalid session %s', sid) - r = self._bad_request() + self._log_error_once('Invalid session ' + sid, 'bad-sid') + r = self._bad_request('Invalid session ' + sid) else: socket = self._get_socket(sid) try: @@ -294,16 +305,7 @@ class AsyncServer(server.Server): getattr(self, '_' + encoding)(r['response']) r['headers'] += [('Content-Encoding', encoding)] break - cors_headers = self._cors_headers(environ) - make_response = self._async['make_response'] - if asyncio.iscoroutinefunction(make_response): - response = await make_response(r['status'], - r['headers'] + cors_headers, - r['response'], environ) - else: - response = make_response(r['status'], r['headers'] + cors_headers, - r['response'], environ) - return response + return await self._make_response(r, environ) def start_background_task(self, target, *args, **kwargs): """Start a background task using the appropriate async model. @@ -362,15 +364,29 @@ class AsyncServer(server.Server): """ return asyncio.Event(*args, **kwargs) - async def _handle_connect(self, environ, transport, b64=False, - jsonp_index=None): + async def _make_response(self, response_dict, environ): + cors_headers = self._cors_headers(environ) + make_response = self._async['make_response'] + if asyncio.iscoroutinefunction(make_response): + response = await make_response( + response_dict['status'], + response_dict['headers'] + cors_headers, + response_dict['response'], environ) + else: + response = make_response( + response_dict['status'], + response_dict['headers'] + cors_headers, + response_dict['response'], environ) + return response + + async def _handle_connect(self, environ, transport, jsonp_index=None): """Handle a client connection request.""" if self.start_service_task: # start the service task to monitor connected clients self.start_service_task = False self.start_background_task(self._service_task) - sid = self._generate_id() + sid = self.generate_id() s = asyncio_socket.AsyncSocket(self, sid) self.sockets[sid] = s @@ -380,17 +396,18 @@ class AsyncServer(server.Server): 'pingTimeout': int(self.ping_timeout * 1000), 'pingInterval': int(self.ping_interval * 1000)}) await s.send(pkt) + s.schedule_ping() ret = await self._trigger_event('connect', sid, environ, run_async=False) - if ret is False: + if ret is not None and ret is not True: del self.sockets[sid] self.logger.warning('Application rejected connection') - return self._unauthorized() + return self._unauthorized(ret or None) if transport == 'websocket': ret = await s.handle_get_request(environ) - if s.closed: + if s.closed and sid in self.sockets: # websocket connection ended, so we are done del self.sockets[sid] return ret @@ -398,9 +415,20 @@ class AsyncServer(server.Server): s.connected = True headers = None if self.cookie: - headers = [('Set-Cookie', self.cookie + '=' + sid)] + if isinstance(self.cookie, dict): + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, self.cookie) + )] + else: + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, { + 'name': self.cookie, 'path': '/', 'SameSite': 'Lax' + }) + )] try: - return self._ok(await s.poll(), headers=headers, b64=b64, + return self._ok(await s.poll(), headers=headers, jsonp_index=jsonp_index) except exceptions.QueueEmpty: return self._bad_request() @@ -459,7 +487,12 @@ class AsyncServer(server.Server): if not socket.closing and not socket.closed: await socket.check_ping_timeout() await self.sleep(sleep_interval) - except (SystemExit, KeyboardInterrupt, asyncio.CancelledError): + except ( + SystemExit, + KeyboardInterrupt, + asyncio.CancelledError, + GeneratorExit, + ): self.logger.info('service task canceled') break except: diff --git a/engineio/asyncio_socket.py b/engineio/asyncio_socket.py index 7057a6c..508ee3c 100644 --- a/engineio/asyncio_socket.py +++ b/engineio/asyncio_socket.py @@ -1,5 +1,4 @@ import asyncio -import six import sys import time @@ -13,18 +12,24 @@ class AsyncSocket(socket.Socket): async def poll(self): """Wait for packets to send to the client.""" try: - packets = [await asyncio.wait_for(self.queue.get(), - self.server.ping_timeout)] + packets = [await asyncio.wait_for( + self.queue.get(), + self.server.ping_interval + self.server.ping_timeout)] self.queue.task_done() except (asyncio.TimeoutError, asyncio.CancelledError): raise exceptions.QueueEmpty() if packets == [None]: return [] - try: - packets.append(self.queue.get_nowait()) - self.queue.task_done() - except asyncio.QueueEmpty: - pass + while True: + try: + pkt = self.queue.get_nowait() + self.queue.task_done() + if pkt is None: + self.queue.put_nowait(None) + break + packets.append(pkt) + except asyncio.QueueEmpty: + break return packets async def receive(self, pkt): @@ -33,9 +38,8 @@ class AsyncSocket(socket.Socket): self.sid, packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '<binary>') - if pkt.packet_type == packet.PING: - self.last_ping = time.time() - await self.send(packet.Packet(packet.PONG, pkt.data)) + if pkt.packet_type == packet.PONG: + self.schedule_ping() elif pkt.packet_type == packet.MESSAGE: await self.server._trigger_event( 'message', self.sid, pkt.data, @@ -48,14 +52,11 @@ class AsyncSocket(socket.Socket): raise exceptions.UnknownPacketError() async def check_ping_timeout(self): - """Make sure the client is still sending pings. - - This helps detect disconnections for long-polling clients. - """ + """Make sure the client is still sending pings.""" if self.closed: raise exceptions.SocketIsClosedError() - if time.time() - self.last_ping > self.server.ping_interval + \ - self.server.ping_interval_grace_period: + if self.last_ping and \ + time.time() - self.last_ping > self.server.ping_timeout: self.server.logger.info('%s: Client is gone, closing socket', self.sid) # Passing abort=False here will cause close() to write a @@ -69,8 +70,6 @@ class AsyncSocket(socket.Socket): """Send a packet to the client.""" if not await self.check_ping_timeout(): return - if self.upgrading: - self.packet_backlog.append(pkt) else: await self.queue.put(pkt) self.server.logger.info('%s: Sending packet %s data %s', @@ -88,12 +87,16 @@ class AsyncSocket(socket.Socket): self.server.logger.info('%s: Received request to upgrade to %s', self.sid, transport) return await getattr(self, '_upgrade_' + transport)(environ) + if self.upgrading or self.upgraded: + # we are upgrading to WebSocket, do not return any more packets + # through the polling endpoint + return [packet.Packet(packet.NOOP)] try: packets = await self.poll() except exceptions.QueueEmpty: exc = sys.exc_info() await self.close(wait=False) - six.reraise(*exc) + raise exc[1].with_traceback(exc[2]) return packets async def handle_post_request(self, environ): @@ -102,7 +105,7 @@ class AsyncSocket(socket.Socket): if length > self.server.max_http_buffer_size: raise exceptions.ContentTooLongError() else: - body = await environ['wsgi.input'].read(length) + body = (await environ['wsgi.input'].read(length)).decode('utf-8') p = payload.Payload(encoded_payload=body) for pkt in p.packets: await self.receive(pkt) @@ -118,6 +121,16 @@ class AsyncSocket(socket.Socket): if wait: await self.queue.join() + def schedule_ping(self): + async def send_ping(): + self.last_ping = None + await asyncio.sleep(self.server.ping_interval) + if not self.closing and not self.closed: + self.last_ping = time.time() + await self.send(packet.Packet(packet.PING)) + + self.server.start_background_task(send_ping) + async def _upgrade_websocket(self, environ): """Upgrade the connection from polling to websocket.""" if self.upgraded: @@ -143,15 +156,15 @@ class AsyncSocket(socket.Socket): decoded_pkt.data != 'probe': self.server.logger.info( '%s: Failed websocket upgrade, no PING packet', self.sid) + self.upgrading = False return - await ws.send(packet.Packet( - packet.PONG, - data=six.text_type('probe')).encode(always_bytes=False)) + await ws.send(packet.Packet(packet.PONG, data='probe').encode()) await self.queue.put(packet.Packet(packet.NOOP)) # end poll try: pkt = await ws.wait() except IOError: # pragma: no cover + self.upgrading = False return decoded_pkt = packet.Packet(encoded_packet=pkt) if decoded_pkt.packet_type != packet.UPGRADE: @@ -160,13 +173,9 @@ class AsyncSocket(socket.Socket): ('%s: Failed websocket upgrade, expected UPGRADE packet, ' 'received %s instead.'), self.sid, pkt) + self.upgrading = False return self.upgraded = True - - # flush any packets that were sent during the upgrade - for pkt in self.packet_backlog: - await self.queue.put(pkt) - self.packet_backlog = [] self.upgrading = False else: self.connected = True @@ -185,7 +194,7 @@ class AsyncSocket(socket.Socket): break try: for pkt in packets: - await ws.send(pkt.encode(always_bytes=False)) + await ws.send(pkt.encode()) except: break writer_task = asyncio.ensure_future(writer()) @@ -197,7 +206,9 @@ class AsyncSocket(socket.Socket): p = None wait_task = asyncio.ensure_future(ws.wait()) try: - p = await asyncio.wait_for(wait_task, self.server.ping_timeout) + p = await asyncio.wait_for( + wait_task, + self.server.ping_interval + self.server.ping_timeout) except asyncio.CancelledError: # pragma: no cover # there is a bug (https://bugs.python.org/issue30508) in # asyncio that causes a "Task exception never retrieved" error @@ -216,8 +227,6 @@ class AsyncSocket(socket.Socket): if p is None: # connection closed by client break - if isinstance(p, six.text_type): # pragma: no cover - p = p.encode('utf-8') pkt = packet.Packet(encoded_packet=p) try: await self.receive(pkt) diff --git a/engineio/client.py b/engineio/client.py index 0071eeb..524c1a6 100644 --- a/engineio/client.py +++ b/engineio/client.py @@ -1,3 +1,5 @@ +from base64 import b64encode +from json import JSONDecodeError import logging try: import queue @@ -7,9 +9,8 @@ import signal import ssl import threading import time +import urllib -import six -from six.moves import urllib try: import requests except ImportError: # pragma: no cover @@ -25,9 +26,6 @@ from . import payload default_logger = logging.getLogger('engineio.client') connected_clients = [] -if six.PY2: # pragma: no cover - ConnectionError = OSError - def signal_handler(sig, frame): """SIGINT handler. @@ -35,10 +33,8 @@ def signal_handler(sig, frame): Disconnect all active clients and then invoke the original signal handler. """ for client in connected_clients[:]: - if client.is_asyncio_based(): - client.start_background_task(client.disconnect, abort=True) - else: - client.disconnect(abort=True) + if not client.is_asyncio_based(): + client.disconnect() if callable(original_signal_handler): return original_signal_handler(sig, frame) else: # pragma: no cover @@ -46,7 +42,7 @@ def signal_handler(sig, frame): return signal.default_int_handler(sig, frame) -original_signal_handler = signal.signal(signal.SIGINT, signal_handler) +original_signal_handler = None class Client(object): @@ -57,13 +53,18 @@ class Client(object): :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. The default is - ``False``. + ``False``. Note that fatal errors are logged even when + ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param request_timeout: A timeout in seconds for requests. The default is 5 seconds. + :param http_session: an initialized ``requests.Session`` object to be used + when sending requests to the server. Use it if you + need to add special client options such as proxy + servers, SSL certificates, etc. :param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to skip SSL certificate verification, allowing connections to servers with self signed certificates. @@ -75,7 +76,13 @@ class Client(object): logger=False, json=None, request_timeout=5, + http_session=None, ssl_verify=True): + global original_signal_handler + if original_signal_handler is None and \ + threading.current_thread() == threading.main_thread(): + original_signal_handler = signal.signal(signal.SIGINT, + signal_handler) self.handlers = {} self.base_url = None self.transports = None @@ -84,13 +91,10 @@ class Client(object): self.upgrades = None self.ping_interval = None self.ping_timeout = None - self.pong_received = True - self.http = None + self.http = http_session self.ws = None self.read_loop_task = None self.write_loop_task = None - self.ping_loop_task = None - self.ping_loop_event = None self.queue = None self.state = 'disconnected' self.ssl_verify = ssl_verify @@ -101,8 +105,7 @@ class Client(object): self.logger = logger else: self.logger = default_logger - if not logging.root.handlers and \ - self.logger.level == logging.NOTSET: + if self.logger.level == logging.NOTSET: if logger: self.logger.setLevel(logging.INFO) else: @@ -147,7 +150,7 @@ class Client(object): return set_handler set_handler(handler) - def connect(self, url, headers={}, transports=None, + def connect(self, url, headers=None, transports=None, engineio_path='engine.io'): """Connect to an Engine.IO server. @@ -172,7 +175,7 @@ class Client(object): raise ValueError('Client is not in a disconnected state') valid_transports = ['polling', 'websocket'] if transports is not None: - if isinstance(transports, six.string_types): + if isinstance(transports, str): transports = [transports] transports = [transport for transport in transports if transport in valid_transports] @@ -181,7 +184,7 @@ class Client(object): self.transports = transports or valid_transports self.queue = self.create_queue() return getattr(self, '_connect_' + self.transports[0])( - url, headers, engineio_path) + url, headers or {}, engineio_path) def wait(self): """Wait until the connection with the server ends. @@ -192,19 +195,14 @@ class Client(object): if self.read_loop_task: self.read_loop_task.join() - def send(self, data, binary=None): + def send(self, data): """Send a message to a client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. - :param binary: ``True`` to send packet as binary, ``False`` to send - as text. If not given, unicode (Python 2) and str - (Python 3) are sent as text, and str (Python 2) and - bytes (Python 3) are sent as binary. """ - self._send_packet(packet.Packet(packet.MESSAGE, data=data, - binary=binary)) + self._send_packet(packet.Packet(packet.MESSAGE, data=data)) def disconnect(self, abort=False): """Disconnect from the server. @@ -289,14 +287,19 @@ class Client(object): raise exceptions.ConnectionError( 'Connection refused by the server') if r.status_code < 200 or r.status_code >= 300: + self._reset() + try: + arg = r.json() + except JSONDecodeError: + arg = None raise exceptions.ConnectionError( 'Unexpected status code {} in server response'.format( - r.status_code)) + r.status_code), arg) try: - p = payload.Payload(encoded_payload=r.content) + p = payload.Payload(encoded_payload=r.content.decode('utf-8')) except ValueError: - six.raise_from(exceptions.ConnectionError( - 'Unexpected response from server'), None) + raise exceptions.ConnectionError( + 'Unexpected response from server') from None open_packet = p.packets[0] if open_packet.packet_type != packet.OPEN: raise exceptions.ConnectionError( @@ -305,8 +308,8 @@ class Client(object): 'Polling connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] - self.ping_interval = open_packet.data['pingInterval'] / 1000.0 - self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0 + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'polling' self.base_url += '&sid=' + self.sid @@ -324,7 +327,6 @@ class Client(object): return # start background tasks associated with this client - self.ping_loop_task = self.start_background_task(self._ping_loop) self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_polling) @@ -348,23 +350,71 @@ class Client(object): self.logger.info( 'Attempting WebSocket connection to ' + websocket_url) - # get the cookies from the long-polling connection so that they can - # also be sent the the WebSocket route + # get cookies and other settings from the long-polling connection + # so that they are preserved when connecting to the WebSocket route cookies = None + extra_options = {} if self.http: + # cookies cookies = '; '.join(["{}={}".format(cookie.name, cookie.value) for cookie in self.http.cookies]) + for header, value in headers.items(): + if header.lower() == 'cookie': + if cookies: + cookies += '; ' + cookies += value + del headers[header] + break + # auth + if 'Authorization' not in headers and self.http.auth is not None: + if not isinstance(self.http.auth, tuple): # pragma: no cover + raise ValueError('Only basic authentication is supported') + basic_auth = '{}:{}'.format( + self.http.auth[0], self.http.auth[1]).encode('utf-8') + basic_auth = b64encode(basic_auth).decode('utf-8') + headers['Authorization'] = 'Basic ' + basic_auth + + # cert + # this can be given as ('certfile', 'keyfile') or just 'certfile' + if isinstance(self.http.cert, tuple): + extra_options['sslopt'] = { + 'certfile': self.http.cert[0], + 'keyfile': self.http.cert[1]} + elif self.http.cert: + extra_options['sslopt'] = {'certfile': self.http.cert} + + # proxies + if self.http.proxies: + proxy_url = None + if websocket_url.startswith('ws://'): + proxy_url = self.http.proxies.get( + 'ws', self.http.proxies.get('http')) + else: # wss:// + proxy_url = self.http.proxies.get( + 'wss', self.http.proxies.get('https')) + if proxy_url: + parsed_url = urllib.parse.urlparse( + proxy_url if '://' in proxy_url + else 'scheme://' + proxy_url) + extra_options['http_proxy_host'] = parsed_url.hostname + extra_options['http_proxy_port'] = parsed_url.port + extra_options['http_proxy_auth'] = ( + (parsed_url.username, parsed_url.password) + if parsed_url.username or parsed_url.password + else None) + + # verify + if not self.http.verify: + self.ssl_verify = False + + if not self.ssl_verify: + extra_options['sslopt'] = {"cert_reqs": ssl.CERT_NONE} try: - if not self.ssl_verify: - ws = websocket.create_connection( - websocket_url + self._get_url_timestamp(), header=headers, - cookie=cookies, sslopt={"cert_reqs": ssl.CERT_NONE}) - else: - ws = websocket.create_connection( - websocket_url + self._get_url_timestamp(), header=headers, - cookie=cookies) - except (ConnectionError, IOError): + ws = websocket.create_connection( + websocket_url + self._get_url_timestamp(), header=headers, + cookie=cookies, enable_multithread=True, **extra_options) + except (ConnectionError, IOError, websocket.WebSocketException): if upgrade: self.logger.warning( 'WebSocket upgrade failed: connection error') @@ -372,8 +422,7 @@ class Client(object): else: raise exceptions.ConnectionError('Connection error') if upgrade: - p = packet.Packet(packet.PING, - data=six.text_type('probe')).encode() + p = packet.Packet(packet.PING, data='probe').encode() try: ws.send(p) except Exception as e: # pragma: no cover @@ -416,17 +465,17 @@ class Client(object): 'WebSocket connection accepted with ' + str(open_packet.data)) self.sid = open_packet.data['sid'] self.upgrades = open_packet.data['upgrades'] - self.ping_interval = open_packet.data['pingInterval'] / 1000.0 - self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0 + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 self.current_transport = 'websocket' self.state = 'connected' connected_clients.append(self) self._trigger_event('connect', run_async=False) self.ws = ws + self.ws.settimeout(self.ping_interval + self.ping_timeout) # start background tasks associated with this client - self.ping_loop_task = self.start_background_task(self._ping_loop) self.write_loop_task = self.start_background_task(self._write_loop) self.read_loop_task = self.start_background_task( self._read_loop_websocket) @@ -441,8 +490,8 @@ class Client(object): pkt.data if not isinstance(pkt.data, bytes) else '<binary>') if pkt.packet_type == packet.MESSAGE: self._trigger_event('message', pkt.data, run_async=True) - elif pkt.packet_type == packet.PONG: - self.pong_received = True + elif pkt.packet_type == packet.PING: + self._send_packet(packet.Packet(packet.PONG, pkt.data)) elif pkt.packet_type == packet.CLOSE: self.disconnect(abort=True) elif pkt.packet_type == packet.NOOP: @@ -500,7 +549,7 @@ class Client(object): scheme += 's' return ('{scheme}://{netloc}/{path}/?{query}' - '{sep}transport={transport}&EIO=3').format( + '{sep}transport={transport}&EIO=4').format( scheme=scheme, netloc=parsed_url.netloc, path=engineio_path, query=parsed_url.query, sep='&' if parsed_url.query else '', @@ -510,28 +559,6 @@ class Client(object): """Generate the Engine.IO query string timestamp.""" return '&t=' + str(time.time()) - def _ping_loop(self): - """This background task sends a PING to the server at the requested - interval. - """ - self.pong_received = True - if self.ping_loop_event is None: - self.ping_loop_event = self.create_event() - else: - self.ping_loop_event.clear() - while self.state == 'connected': - if not self.pong_received: - self.logger.info( - 'PONG response has not been received, aborting') - if self.ws: - self.ws.shutdown() - self.queue.put(None) - break - self.pong_received = False - self._send_packet(packet.Packet(packet.PING)) - self.ping_loop_event.wait(timeout=self.ping_interval) - self.logger.info('Exiting ping task') - def _read_loop_polling(self): """Read packets by polling the Engine.IO server.""" while self.state == 'connected': @@ -551,7 +578,7 @@ class Client(object): self.queue.put(None) break try: - p = payload.Payload(encoded_payload=r.content) + p = payload.Payload(encoded_payload=r.content.decode('utf-8')) except ValueError: self.logger.warning( 'Unexpected packet from server, aborting') @@ -562,10 +589,6 @@ class Client(object): self.logger.info('Waiting for write loop task to end') self.write_loop_task.join() - self.logger.info('Waiting for ping loop task to end') - if self.ping_loop_event: # pragma: no cover - self.ping_loop_event.set() - self.ping_loop_task.join() if self.state == 'connected': self._trigger_event('disconnect', run_async=False) try: @@ -581,6 +604,11 @@ class Client(object): p = None try: p = self.ws.recv() + except websocket.WebSocketTimeoutException: + self.logger.warning( + 'Server has stopped communicating, aborting') + self.queue.put(None) + break except websocket.WebSocketConnectionClosedException: self.logger.warning( 'WebSocket connection was closed, aborting') @@ -588,20 +616,21 @@ class Client(object): break except Exception as e: self.logger.info( - 'Unexpected error "%s", aborting', str(e)) + 'Unexpected error receiving packet: "%s", aborting', + str(e)) + self.queue.put(None) + break + try: + pkt = packet.Packet(encoded_packet=p) + except Exception as e: # pragma: no cover + self.logger.info( + 'Unexpected error decoding packet: "%s", aborting', str(e)) self.queue.put(None) break - if isinstance(p, six.text_type): # pragma: no cover - p = p.encode('utf-8') - pkt = packet.Packet(encoded_packet=p) self._receive_packet(pkt) self.logger.info('Waiting for write loop task to end') self.write_loop_task.join() - self.logger.info('Waiting for ping loop task to end') - if self.ping_loop_event: # pragma: no cover - self.ping_loop_event.set() - self.ping_loop_task.join() if self.state == 'connected': self._trigger_event('disconnect', run_async=False) try: @@ -663,13 +692,14 @@ class Client(object): # websocket try: for pkt in packets: - encoded_packet = pkt.encode(always_bytes=False) + encoded_packet = pkt.encode() if pkt.binary: self.ws.send_binary(encoded_packet) else: self.ws.send(encoded_packet) self.queue.task_done() - except websocket.WebSocketConnectionClosedException: + except (websocket.WebSocketConnectionClosedException, + BrokenPipeError, OSError): self.logger.warning( 'WebSocket connection was closed, aborting') break diff --git a/engineio/packet.py b/engineio/packet.py index a3aa6d4..9dbd6c6 100644 --- a/engineio/packet.py +++ b/engineio/packet.py @@ -1,12 +1,10 @@ import base64 import json as _json -import six - (OPEN, CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP) = (0, 1, 2, 3, 4, 5, 6) packet_names = ['OPEN', 'CLOSE', 'PING', 'PONG', 'MESSAGE', 'UPGRADE', 'NOOP'] -binary_types = (six.binary_type, bytearray) +binary_types = (bytes, bytearray) class Packet(object): @@ -14,79 +12,61 @@ class Packet(object): json = _json - def __init__(self, packet_type=NOOP, data=None, binary=None, - encoded_packet=None): + def __init__(self, packet_type=NOOP, data=None, encoded_packet=None): self.packet_type = packet_type self.data = data - if binary is not None: - self.binary = binary - elif isinstance(data, six.text_type): + if isinstance(data, str): self.binary = False elif isinstance(data, binary_types): self.binary = True else: self.binary = False + if self.binary and self.packet_type != MESSAGE: + raise ValueError('Binary packets can only be of type MESSAGE') if encoded_packet: self.decode(encoded_packet) - def encode(self, b64=False, always_bytes=True): + def encode(self, b64=False): """Encode the packet for transmission.""" - if self.binary and not b64: - encoded_packet = six.int2byte(self.packet_type) - else: - encoded_packet = six.text_type(self.packet_type) - if self.binary and b64: - encoded_packet = 'b' + encoded_packet if self.binary: if b64: - encoded_packet += base64.b64encode(self.data).decode('utf-8') + encoded_packet = 'b' + base64.b64encode(self.data).decode( + 'utf-8') else: + encoded_packet = self.data + else: + encoded_packet = str(self.packet_type) + if isinstance(self.data, str): encoded_packet += self.data - elif isinstance(self.data, six.string_types): - encoded_packet += self.data - elif isinstance(self.data, dict) or isinstance(self.data, list): - encoded_packet += self.json.dumps(self.data, - separators=(',', ':')) - elif self.data is not None: - encoded_packet += str(self.data) - if always_bytes and not isinstance(encoded_packet, binary_types): - encoded_packet = encoded_packet.encode('utf-8') + elif isinstance(self.data, dict) or isinstance(self.data, list): + encoded_packet += self.json.dumps(self.data, + separators=(',', ':')) + elif self.data is not None: + encoded_packet += str(self.data) return encoded_packet def decode(self, encoded_packet): """Decode a transmitted package.""" - b64 = False - if not isinstance(encoded_packet, binary_types): - encoded_packet = encoded_packet.encode('utf-8') - elif not isinstance(encoded_packet, bytes): - encoded_packet = bytes(encoded_packet) - self.packet_type = six.byte2int(encoded_packet[0:1]) - if self.packet_type == 98: # 'b' --> binary base64 encoded packet + self.binary = isinstance(encoded_packet, binary_types) + b64 = not self.binary and encoded_packet[0] == 'b' + if b64: self.binary = True - encoded_packet = encoded_packet[1:] - self.packet_type = six.byte2int(encoded_packet[0:1]) - self.packet_type -= 48 - b64 = True - elif self.packet_type >= 48: - self.packet_type -= 48 - self.binary = False + self.packet_type = MESSAGE + self.data = base64.b64decode(encoded_packet[1:]) else: - self.binary = True - self.data = None - if len(encoded_packet) > 1: + if self.binary and not isinstance(encoded_packet, bytes): + encoded_packet = bytes(encoded_packet) if self.binary: - if b64: - self.data = base64.b64decode(encoded_packet[1:]) - else: - self.data = encoded_packet[1:] + self.packet_type = MESSAGE + self.data = encoded_packet else: + self.packet_type = int(encoded_packet[0]) try: - self.data = self.json.loads( - encoded_packet[1:].decode('utf-8')) + self.data = self.json.loads(encoded_packet[1:]) if isinstance(self.data, int): # do not allow integer payloads, see # github.com/miguelgrinberg/python-engineio/issues/75 # for background on this decision raise ValueError except ValueError: - self.data = encoded_packet[1:].decode('utf-8') + self.data = encoded_packet[1:] diff --git a/engineio/payload.py b/engineio/payload.py index fbf9cbd..f0e9e34 100644 --- a/engineio/payload.py +++ b/engineio/payload.py @@ -1,9 +1,7 @@ -import six +import urllib from . import packet -from six.moves import urllib - class Payload(object): """Engine.IO payload.""" @@ -14,31 +12,19 @@ class Payload(object): if encoded_payload is not None: self.decode(encoded_payload) - def encode(self, b64=False, jsonp_index=None): + def encode(self, jsonp_index=None): """Encode the payload for transmission.""" - encoded_payload = b'' + encoded_payload = '' for pkt in self.packets: - encoded_packet = pkt.encode(b64=b64) - packet_len = len(encoded_packet) - if b64: - encoded_payload += str(packet_len).encode('utf-8') + b':' + \ - encoded_packet - else: - binary_len = b'' - while packet_len != 0: - binary_len = six.int2byte(packet_len % 10) + binary_len - packet_len = int(packet_len / 10) - if not pkt.binary: - encoded_payload += b'\0' - else: - encoded_payload += b'\1' - encoded_payload += binary_len + b'\xff' + encoded_packet + if encoded_payload: + encoded_payload += '\x1e' + encoded_payload += pkt.encode(b64=True) if jsonp_index is not None: - encoded_payload = b'___eio[' + \ - str(jsonp_index).encode() + \ - b']("' + \ - encoded_payload.replace(b'"', b'\\"') + \ - b'");' + encoded_payload = '___eio[' + \ + str(jsonp_index) + \ + ']("' + \ + encoded_payload.replace('"', '\\"') + \ + '");' return encoded_payload def decode(self, encoded_payload): @@ -49,33 +35,12 @@ class Payload(object): return # JSONP POST payload starts with 'd=' - if encoded_payload.startswith(b'd='): + if encoded_payload.startswith('d='): encoded_payload = urllib.parse.parse_qs( - encoded_payload)[b'd'][0] + encoded_payload)['d'][0] - i = 0 - if six.byte2int(encoded_payload[0:1]) <= 1: - # binary encoding - while i < len(encoded_payload): - if len(self.packets) >= self.max_decode_packets: - raise ValueError('Too many packets in payload') - packet_len = 0 - i += 1 - while six.byte2int(encoded_payload[i:i + 1]) != 255: - packet_len = packet_len * 10 + six.byte2int( - encoded_payload[i:i + 1]) - i += 1 - self.packets.append(packet.Packet( - encoded_packet=encoded_payload[i + 1:i + 1 + packet_len])) - i += packet_len + 1 - else: - # assume text encoding - encoded_payload = encoded_payload.decode('utf-8') - while i < len(encoded_payload): - if len(self.packets) >= self.max_decode_packets: - raise ValueError('Too many packets in payload') - j = encoded_payload.find(':', i) - packet_len = int(encoded_payload[i:j]) - pkt = encoded_payload[j + 1:j + 1 + packet_len] - self.packets.append(packet.Packet(encoded_packet=pkt)) - i = j + 1 + packet_len + encoded_packets = encoded_payload.split('\x1e') + if len(encoded_packets) > self.max_decode_packets: + raise ValueError('Too many packets in payload') + self.packets = [packet.Packet(encoded_packet=encoded_packet) + for encoded_packet in encoded_packets] diff --git a/engineio/server.py b/engineio/server.py index e1543c2..703e7e9 100644 --- a/engineio/server.py +++ b/engineio/server.py @@ -1,12 +1,12 @@ +import base64 import gzip import importlib +import io import logging -import uuid +import secrets +import urllib import zlib -import six -from six.moves import urllib - from . import exceptions from . import packet from . import payload @@ -29,17 +29,16 @@ class Server(object): "gevent_uwsgi", then "gevent", and finally "threading". The first async mode that has all its dependencies installed is the one that is chosen. - :param ping_timeout: The time in seconds that the client waits for the - server to respond before disconnecting. The default - is 60 seconds. - :param ping_interval: The interval in seconds at which the client pings - the server. The default is 25 seconds. For advanced + :param ping_interval: The interval in seconds at which the server pings + the client. The default is 25 seconds. For advanced control, a two element tuple can be given, where the first number is the ping interval and the second - is a grace period added by the server. The default - grace period is 5 seconds. + is a grace period added by the server. + :param ping_timeout: The time in seconds that the client waits for the + server to respond before disconnecting. The default + is 5 seconds. :param max_http_buffer_size: The maximum size of a message when using the - polling transport. The default is 100,000,000 + polling transport. The default is 1,000,000 bytes. :param allow_upgrades: Whether to allow transport upgrades or not. The default is ``True``. @@ -48,9 +47,14 @@ class Server(object): :param compression_threshold: Only compress messages when their byte size is greater than this value. The default is 1024 bytes. - :param cookie: Name of the HTTP cookie that contains the client session - id. If set to ``None``, a cookie is not sent to the client. - The default is ``'io'``. + :param cookie: If set to a string, it is the name of the HTTP cookie the + server sends back tot he client containing the client + session id. If set to a dictionary, the ``'name'`` key + contains the cookie name and other keys define cookie + attributes, where the value of each attribute can be a + string, a callable with no arguments, or a boolean. If set + to ``None`` (the default), a cookie is not sent to the + client. :param cors_allowed_origins: Origin or list of origins that are allowed to connect to this server. Only the same origin is allowed by default. Set this argument to @@ -61,7 +65,8 @@ class Server(object): is ``True``. :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. The default is - ``False``. + ``False``. Note that fatal errors are logged even when + ``logger`` is ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library @@ -79,11 +84,12 @@ class Server(object): compression_methods = ['gzip', 'deflate'] event_names = ['connect', 'disconnect', 'message'] _default_monitor_clients = True + sequence_number = 0 - def __init__(self, async_mode=None, ping_timeout=60, ping_interval=25, - max_http_buffer_size=100000000, allow_upgrades=True, + def __init__(self, async_mode=None, ping_interval=25, ping_timeout=5, + max_http_buffer_size=1000000, allow_upgrades=True, http_compression=True, compression_threshold=1024, - cookie='io', cors_allowed_origins=None, + cookie=None, cors_allowed_origins=None, cors_credentials=True, logger=False, json=None, async_handlers=True, monitor_clients=None, **kwargs): self.ping_timeout = ping_timeout @@ -92,7 +98,7 @@ class Server(object): self.ping_interval_grace_period = ping_interval[1] else: self.ping_interval = ping_interval - self.ping_interval_grace_period = 5 + self.ping_interval_grace_period = 0 self.max_http_buffer_size = max_http_buffer_size self.allow_upgrades = allow_upgrades self.http_compression = http_compression @@ -103,6 +109,7 @@ class Server(object): self.async_handlers = async_handlers self.sockets = {} self.handlers = {} + self.log_message_keys = set() self.start_service_task = monitor_clients \ if monitor_clients is not None else self._default_monitor_clients if json is not None: @@ -111,8 +118,7 @@ class Server(object): self.logger = logger else: self.logger = default_logger - if not logging.root.handlers and \ - self.logger.level == logging.NOTSET: + if self.logger.level == logging.NOTSET: if logger: self.logger.setLevel(logging.INFO) else: @@ -196,17 +202,13 @@ class Server(object): return set_handler set_handler(handler) - def send(self, sid, data, binary=None): + def send(self, sid, data): """Send a message to a client. :param sid: The session id of the recipient client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. - :param binary: ``True`` to send packet as binary, ``False`` to send - as text. If not given, unicode (Python 2) and str - (Python 3) are sent as text, and str (Python 2) and - bytes (Python 3) are sent as binary. """ try: socket = self._get_socket(sid) @@ -214,7 +216,7 @@ class Server(object): # the socket is not available self.logger.warning('Cannot send to sid %s', sid) return - socket.send(packet.Packet(packet.MESSAGE, data=data, binary=binary)) + socket.send(packet.Packet(packet.MESSAGE, data=data)) def get_session(self, sid): """Return the user session for a client. @@ -292,7 +294,7 @@ class Server(object): if sid in self.sockets: # pragma: no cover del self.sockets[sid] else: - for client in six.itervalues(self.sockets): + for client in self.sockets.values(): client.close() self.sockets = {} @@ -329,22 +331,30 @@ class Server(object): allowed_origins = self._cors_allowed_origins(environ) if allowed_origins is not None and origin not in \ allowed_origins: - self.logger.info(origin + ' is not an accepted origin.') - r = self._bad_request() + self._log_error_once( + origin + ' is not an accepted origin.', 'bad-origin') + r = self._bad_request( + origin + ' is not an accepted origin.') start_response(r['status'], r['headers']) return [r['response']] method = environ['REQUEST_METHOD'] query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) - - sid = query['sid'][0] if 'sid' in query else None - b64 = False jsonp = False jsonp_index = None - if 'b64' in query: - if query['b64'][0] == "1" or query['b64'][0].lower() == "true": - b64 = True + # make sure the client speaks a compatible Engine.IO version + sid = query['sid'][0] if 'sid' in query else None + if sid is None and query.get('EIO') != ['4']: + self._log_error_once( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols', 'bad-version') + r = self._bad_request( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols') + start_response(r['status'], r['headers']) + return [r['response']] + if 'j' in query: jsonp = True try: @@ -354,29 +364,35 @@ class Server(object): pass if jsonp and jsonp_index is None: - self.logger.warning('Invalid JSONP index number') - r = self._bad_request() + self._log_error_once('Invalid JSONP index number', + 'bad-jsonp-index') + r = self._bad_request('Invalid JSONP index number') elif method == 'GET': if sid is None: transport = query.get('transport', ['polling'])[0] - if transport != 'polling' and transport != 'websocket': - self.logger.warning('Invalid transport %s', transport) - r = self._bad_request() - else: + # transport must be one of 'polling' or 'websocket'. + # if 'websocket', the HTTP_UPGRADE header must match. + upgrade_header = environ.get('HTTP_UPGRADE').lower() \ + if 'HTTP_UPGRADE' in environ else None + if transport == 'polling' \ + or transport == upgrade_header == 'websocket': r = self._handle_connect(environ, start_response, - transport, b64, jsonp_index) + transport, jsonp_index) + else: + self._log_error_once('Invalid transport ' + transport, + 'bad-transport') + r = self._bad_request('Invalid transport ' + transport) else: if sid not in self.sockets: - self.logger.warning('Invalid session %s', sid) - r = self._bad_request() + self._log_error_once('Invalid session ' + sid, 'bad-sid') + r = self._bad_request('Invalid session ' + sid) else: socket = self._get_socket(sid) try: packets = socket.handle_get_request( environ, start_response) if isinstance(packets, list): - r = self._ok(packets, b64=b64, - jsonp_index=jsonp_index) + r = self._ok(packets, jsonp_index=jsonp_index) else: r = packets except exceptions.EngineIOError: @@ -387,8 +403,8 @@ class Server(object): del self.sockets[sid] elif method == 'POST': if sid is None or sid not in self.sockets: - self.logger.warning('Invalid session %s', sid) - r = self._bad_request() + self._log_error_once('Invalid session ' + sid, 'bad-sid') + r = self._bad_request('Invalid session ' + sid) else: socket = self._get_socket(sid) try: @@ -481,11 +497,28 @@ class Server(object): """ return self._async['event'](*args, **kwargs) - def _generate_id(self): + def generate_id(self): """Generate a unique session id.""" - return uuid.uuid4().hex + id = base64.b64encode( + secrets.token_bytes(12) + self.sequence_number.to_bytes(3, 'big')) + self.sequence_number = (self.sequence_number + 1) & 0xffffff + return id.decode('utf-8').replace('/', '_').replace('+', '-') + + def _generate_sid_cookie(self, sid, attributes): + """Generate the sid cookie.""" + cookie = attributes.get('name', 'io') + '=' + sid + for attribute, value in attributes.items(): + if attribute == 'name': + continue + if callable(value): + value = value() + if value is True: + cookie += '; ' + attribute + else: + cookie += '; ' + attribute + '=' + value + return cookie - def _handle_connect(self, environ, start_response, transport, b64=False, + def _handle_connect(self, environ, start_response, transport, jsonp_index=None): """Handle a client connection request.""" if self.start_service_task: @@ -493,36 +526,53 @@ class Server(object): self.start_service_task = False self.start_background_task(self._service_task) - sid = self._generate_id() + sid = self.generate_id() s = socket.Socket(self, sid) self.sockets[sid] = s - pkt = packet.Packet( - packet.OPEN, {'sid': sid, - 'upgrades': self._upgrades(sid, transport), - 'pingTimeout': int(self.ping_timeout * 1000), - 'pingInterval': int(self.ping_interval * 1000)}) + pkt = packet.Packet(packet.OPEN, { + 'sid': sid, + 'upgrades': self._upgrades(sid, transport), + 'pingTimeout': int(self.ping_timeout * 1000), + 'pingInterval': int( + self.ping_interval + self.ping_interval_grace_period) * 1000}) s.send(pkt) + s.schedule_ping() + # NOTE: some sections below are marked as "no cover" to workaround + # what seems to be a bug in the coverage package. All the lines below + # are covered by tests, but some are not reported as such for some + # reason ret = self._trigger_event('connect', sid, environ, run_async=False) - if ret is False: + if ret is not None and ret is not True: # pragma: no cover del self.sockets[sid] self.logger.warning('Application rejected connection') - return self._unauthorized() + return self._unauthorized(ret or None) - if transport == 'websocket': + if transport == 'websocket': # pragma: no cover ret = s.handle_get_request(environ, start_response) - if s.closed: + if s.closed and sid in self.sockets: # websocket connection ended, so we are done del self.sockets[sid] return ret - else: + else: # pragma: no cover s.connected = True headers = None if self.cookie: - headers = [('Set-Cookie', self.cookie + '=' + sid)] + if isinstance(self.cookie, dict): + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, self.cookie) + )] + else: + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, { + 'name': self.cookie, 'path': '/', 'SameSite': 'Lax' + }) + )] try: - return self._ok(s.poll(), headers=headers, b64=b64, + return self._ok(s.poll(), headers=headers, jsonp_index=jsonp_index) except exceptions.QueueEmpty: return self._bad_request() @@ -561,29 +611,29 @@ class Server(object): raise KeyError('Session is disconnected') return s - def _ok(self, packets=None, headers=None, b64=False, jsonp_index=None): + def _ok(self, packets=None, headers=None, jsonp_index=None): """Generate a successful HTTP response.""" if packets is not None: if headers is None: headers = [] - if b64: - headers += [('Content-Type', 'text/plain; charset=UTF-8')] - else: - headers += [('Content-Type', 'application/octet-stream')] + headers += [('Content-Type', 'text/plain; charset=UTF-8')] return {'status': '200 OK', 'headers': headers, 'response': payload.Payload(packets=packets).encode( - b64=b64, jsonp_index=jsonp_index)} + jsonp_index=jsonp_index).encode('utf-8')} else: return {'status': '200 OK', 'headers': [('Content-Type', 'text/plain')], 'response': b'OK'} - def _bad_request(self): + def _bad_request(self, message=None): """Generate a bad request HTTP error response.""" + if message is None: + message = 'Bad Request' + message = packet.Packet.json.dumps(message) return {'status': '400 BAD REQUEST', 'headers': [('Content-Type', 'text/plain')], - 'response': b'Bad Request'} + 'response': message.encode('utf-8')} def _method_not_found(self): """Generate a method not found HTTP error response.""" @@ -591,11 +641,14 @@ class Server(object): 'headers': [('Content-Type', 'text/plain')], 'response': b'Method Not Found'} - def _unauthorized(self): + def _unauthorized(self, message=None): """Generate a unauthorized HTTP error response.""" + if message is None: + message = 'Unauthorized' + message = packet.Packet.json.dumps(message) return {'status': '401 UNAUTHORIZED', - 'headers': [('Content-Type', 'text/plain')], - 'response': b'Unauthorized'} + 'headers': [('Content-Type', 'application/json')], + 'response': message.encode('utf-8')} def _cors_allowed_origins(self, environ): default_origins = [] @@ -613,7 +666,7 @@ class Server(object): allowed_origins = default_origins elif self.cors_allowed_origins == '*': allowed_origins = None - elif isinstance(self.cors_allowed_origins, six.string_types): + elif isinstance(self.cors_allowed_origins, str): allowed_origins = [self.cors_allowed_origins] else: allowed_origins = self.cors_allowed_origins @@ -641,7 +694,7 @@ class Server(object): def _gzip(self, response): """Apply gzip compression to a response.""" - bytesio = six.BytesIO() + bytesio = io.BytesIO() with gzip.GzipFile(fileobj=bytesio, mode='w') as gz: gz.write(response) return bytesio.getvalue() @@ -650,6 +703,16 @@ class Server(object): """Apply deflate compression to a response.""" return zlib.compress(response) + def _log_error_once(self, message, message_key): + """Log message with logging.ERROR level the first time, then log + with given level.""" + if message_key not in self.log_message_keys: + self.logger.error(message + ' (further occurrences of this error ' + 'will be logged with level INFO)') + self.log_message_keys.add(message_key) + else: + self.logger.info(message) + def _service_task(self): # pragma: no cover """Monitor connected clients and clean up those that time out.""" while True: diff --git a/engineio/socket.py b/engineio/socket.py index 38593e7..1434b19 100644 --- a/engineio/socket.py +++ b/engineio/socket.py @@ -1,4 +1,3 @@ -import six import sys import time @@ -15,11 +14,10 @@ class Socket(object): self.server = server self.sid = sid self.queue = self.server.create_queue() - self.last_ping = time.time() + self.last_ping = None self.connected = False self.upgrading = False self.upgraded = False - self.packet_backlog = [] self.closing = False self.closed = False self.session = {} @@ -28,7 +26,8 @@ class Socket(object): """Wait for packets to send to the client.""" queue_empty = self.server.get_queue_empty_exception() try: - packets = [self.queue.get(timeout=self.server.ping_timeout)] + packets = [self.queue.get( + timeout=self.server.ping_interval + self.server.ping_timeout)] self.queue.task_done() except queue_empty: raise exceptions.QueueEmpty() @@ -36,8 +35,12 @@ class Socket(object): return [] while True: try: - packets.append(self.queue.get(block=False)) + pkt = self.queue.get(block=False) self.queue.task_done() + if pkt is None: + self.queue.put(None) + break + packets.append(pkt) except queue_empty: break return packets @@ -50,9 +53,8 @@ class Socket(object): self.sid, packet_name, pkt.data if not isinstance(pkt.data, bytes) else '<binary>') - if pkt.packet_type == packet.PING: - self.last_ping = time.time() - self.send(packet.Packet(packet.PONG, pkt.data)) + if pkt.packet_type == packet.PONG: + self.schedule_ping() elif pkt.packet_type == packet.MESSAGE: self.server._trigger_event('message', self.sid, pkt.data, run_async=self.server.async_handlers) @@ -64,14 +66,11 @@ class Socket(object): raise exceptions.UnknownPacketError() def check_ping_timeout(self): - """Make sure the client is still sending pings. - - This helps detect disconnections for long-polling clients. - """ + """Make sure the client is still responding to pings.""" if self.closed: raise exceptions.SocketIsClosedError() - if time.time() - self.last_ping > self.server.ping_interval + \ - self.server.ping_interval_grace_period: + if self.last_ping and \ + time.time() - self.last_ping > self.server.ping_timeout: self.server.logger.info('%s: Client is gone, closing socket', self.sid) # Passing abort=False here will cause close() to write a @@ -85,8 +84,6 @@ class Socket(object): """Send a packet to the client.""" if not self.check_ping_timeout(): return - if self.upgrading: - self.packet_backlog.append(pkt) else: self.queue.put(pkt) self.server.logger.info('%s: Sending packet %s data %s', @@ -105,12 +102,16 @@ class Socket(object): self.sid, transport) return getattr(self, '_upgrade_' + transport)(environ, start_response) + if self.upgrading or self.upgraded: + # we are upgrading to WebSocket, do not return any more packets + # through the polling endpoint + return [packet.Packet(packet.NOOP)] try: packets = self.poll() except exceptions.QueueEmpty: exc = sys.exc_info() self.close(wait=False) - six.reraise(*exc) + raise exc[1].with_traceback(exc[2]) return packets def handle_post_request(self, environ): @@ -119,7 +120,7 @@ class Socket(object): if length > self.server.max_http_buffer_size: raise exceptions.ContentTooLongError() else: - body = environ['wsgi.input'].read(length) + body = environ['wsgi.input'].read(length).decode('utf-8') p = payload.Payload(encoded_payload=body) for pkt in p.packets: self.receive(pkt) @@ -136,6 +137,16 @@ class Socket(object): if wait: self.queue.join() + def schedule_ping(self): + def send_ping(): + self.last_ping = None + self.server.sleep(self.server.ping_interval) + if not self.closing and not self.closed: + self.last_ping = time.time() + self.send(packet.Packet(packet.PING)) + + self.server.start_background_task(send_ping) + def _upgrade_websocket(self, environ, start_response): """Upgrade the connection from polling to websocket.""" if self.upgraded: @@ -149,9 +160,11 @@ class Socket(object): def _websocket_handler(self, ws): """Engine.IO handler for websocket transport.""" # try to set a socket timeout matching the configured ping interval + # and timeout for attr in ['_sock', 'socket']: # pragma: no cover if hasattr(ws, attr) and hasattr(getattr(ws, attr), 'settimeout'): - getattr(ws, attr).settimeout(self.server.ping_timeout) + getattr(ws, attr).settimeout( + self.server.ping_interval + self.server.ping_timeout) if self.connected: # the socket was already connected, so this is an upgrade @@ -163,10 +176,9 @@ class Socket(object): decoded_pkt.data != 'probe': self.server.logger.info( '%s: Failed websocket upgrade, no PING packet', self.sid) + self.upgrading = False return [] - ws.send(packet.Packet( - packet.PONG, - data=six.text_type('probe')).encode(always_bytes=False)) + ws.send(packet.Packet(packet.PONG, data='probe').encode()) self.queue.put(packet.Packet(packet.NOOP)) # end poll pkt = ws.wait() @@ -177,13 +189,9 @@ class Socket(object): ('%s: Failed websocket upgrade, expected UPGRADE packet, ' 'received %s instead.'), self.sid, pkt) + self.upgrading = False return [] self.upgraded = True - - # flush any packets that were sent during the upgrade - for pkt in self.packet_backlog: - self.queue.put(pkt) - self.packet_backlog = [] self.upgrading = False else: self.connected = True @@ -202,7 +210,7 @@ class Socket(object): break try: for pkt in packets: - ws.send(pkt.encode(always_bytes=False)) + ws.send(pkt.encode()) except: break writer_task = self.server.start_background_task(writer) @@ -225,8 +233,6 @@ class Socket(object): if p is None: # connection closed by client break - if isinstance(p, six.text_type): # pragma: no cover - p = p.encode('utf-8') pkt = packet.Packet(encoded_packet=p) try: self.receive(pkt) diff --git a/python_engineio.egg-info/PKG-INFO b/python_engineio.egg-info/PKG-INFO index d913b64..4aca5ae 100644 --- a/python_engineio.egg-info/PKG-INFO +++ b/python_engineio.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: python-engineio -Version: 3.11.1 +Version: 4.0.0 Summary: Engine.IO server Home-page: http://github.com/miguelgrinberg/python-engineio/ Author: Miguel Grinberg @@ -9,8 +9,11 @@ License: MIT Description: python-engineio =============== - .. image:: https://travis-ci.org/miguelgrinberg/python-engineio.svg?branch=master - :target: https://travis-ci.org/miguelgrinberg/python-engineio + .. image:: https://github.com/miguelgrinberg/python-engineio/workflows/build/badge.svg + :target: https://github.com/miguelgrinberg/python-engineio/actions + + .. image:: https://codecov.io/gh/miguelgrinberg/python-engineio/branch/master/graph/badge.svg + :target: https://codecov.io/gh/miguelgrinberg/python-engineio Python implementation of the `Engine.IO`_ realtime client and server. @@ -39,5 +42,5 @@ Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules -Provides-Extra: asyncio_client Provides-Extra: client +Provides-Extra: asyncio_client diff --git a/python_engineio.egg-info/requires.txt b/python_engineio.egg-info/requires.txt index 44b827e..bea152f 100644 --- a/python_engineio.egg-info/requires.txt +++ b/python_engineio.egg-info/requires.txt @@ -1,4 +1,3 @@ -six>=1.9.0 [asyncio_client] aiohttp>=3.4 @@ -29,9 +29,7 @@ setup( zip_safe=False, include_package_data=True, platforms='any', - install_requires=[ - 'six>=1.9.0', - ], + install_requires=[], extras_require={ 'client': [ 'requests>=2.21.0', |
