From 514540b90b2ce09e8c00a1e8535c9f4df378a16f Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Sat, 27 May 2017 15:32:37 +0200 Subject: [PATCH 01/18] add debug for header values --- sanic/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/server.py b/sanic/server.py index 36b2a7a2..5ce15a1b 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -164,6 +164,8 @@ class HttpProtocol(asyncio.Protocol): self.url = url def on_header(self, name, value): + print(f'header "{name}": "{value}"') + if name == b'Content-Length' and int(value) > self.request_max_size: exception = PayloadTooLarge('Payload Too Large') self.write_error(exception) From dc411651b6a1463880d7a1e82c9ce9da939a9167 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Sat, 27 May 2017 15:36:57 +0200 Subject: [PATCH 02/18] add check for header and value --- sanic/server.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 5ce15a1b..c2950ad0 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -166,11 +166,12 @@ class HttpProtocol(asyncio.Protocol): def on_header(self, name, value): print(f'header "{name}": "{value}"') - if name == b'Content-Length' and int(value) > self.request_max_size: - exception = PayloadTooLarge('Payload Too Large') - self.write_error(exception) + if name and value: + if name == b'Content-Length' and int(value) > self.request_max_size: + exception = PayloadTooLarge('Payload Too Large') + self.write_error(exception) - self.headers.append((name.decode().casefold(), value.decode())) + self.headers.append((name.decode().casefold(), value.decode())) def on_headers_complete(self): self.request = self.request_class( From 53a04309ffe2496f711fbbeba0dd48771a7d98ed Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Sat, 27 May 2017 16:28:57 +0200 Subject: [PATCH 03/18] add header_fragment handeling --- sanic/server.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index c2950ad0..653b2ad9 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -98,6 +98,7 @@ class HttpProtocol(asyncio.Protocol): self._request_handler_task = None self._request_stream_task = None self._keep_alive = keep_alive + self._header_fragment = b'' @property def keep_alive(self): @@ -164,14 +165,20 @@ class HttpProtocol(asyncio.Protocol): self.url = url def on_header(self, name, value): - print(f'header "{name}": "{value}"') + log.debug('on_header', name, value) - if name and value: - if name == b'Content-Length' and int(value) > self.request_max_size: + self._header_fragment += name + + if value is not None: + if self._header_fragment == b'Content-Length' and int(value) > self.request_max_size: exception = PayloadTooLarge('Payload Too Large') self.write_error(exception) - self.headers.append((name.decode().casefold(), value.decode())) + self.headers.append((self._header_fragment.decode().casefold(), value.decode())) + + log.debug('added header', self._header_fragment, value) + + self._header_fragment = b'' def on_headers_complete(self): self.request = self.request_class( From 1b33e05f7434dda7448ec42f9db64cdce73f7f21 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Sat, 27 May 2017 16:32:39 +0200 Subject: [PATCH 04/18] fix debug log messages --- sanic/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 653b2ad9..699831bd 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -165,7 +165,7 @@ class HttpProtocol(asyncio.Protocol): self.url = url def on_header(self, name, value): - log.debug('on_header', name, value) + log.debug('on_header: %s %s', name, value) self._header_fragment += name @@ -176,7 +176,7 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((self._header_fragment.decode().casefold(), value.decode())) - log.debug('added header', self._header_fragment, value) + log.debug('added header: %s %s', self._header_fragment, value) self._header_fragment = b'' From 0e5c7a62cb53bf097c341b28ed6c4f3de09580e7 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Sat, 27 May 2017 22:36:08 +0200 Subject: [PATCH 05/18] remove debug messages --- sanic/server.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 699831bd..a4105f89 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -165,8 +165,6 @@ class HttpProtocol(asyncio.Protocol): self.url = url def on_header(self, name, value): - log.debug('on_header: %s %s', name, value) - self._header_fragment += name if value is not None: @@ -176,8 +174,6 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((self._header_fragment.decode().casefold(), value.decode())) - log.debug('added header: %s %s', self._header_fragment, value) - self._header_fragment = b'' def on_headers_complete(self): From aaef2fbd011b3330d2a5a82b8446f4abb5f833b4 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Sun, 28 May 2017 18:46:07 +0200 Subject: [PATCH 06/18] fix flake8 errors --- sanic/server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index a4105f89..e0bc4238 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -168,11 +168,14 @@ class HttpProtocol(asyncio.Protocol): self._header_fragment += name if value is not None: - if self._header_fragment == b'Content-Length' and int(value) > self.request_max_size: + if self._header_fragment == b'Content-Length' \ + and int(value) > self.request_max_size: exception = PayloadTooLarge('Payload Too Large') self.write_error(exception) - self.headers.append((self._header_fragment.decode().casefold(), value.decode())) + self.headers.append( + (self._header_fragment.decode().casefold(), + value.decode())) self._header_fragment = b'' From 30c2c89c6bbf9a384328280d325c74cb2c9a9e22 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Tue, 30 May 2017 16:13:49 +0200 Subject: [PATCH 07/18] fix partial url parsing --- sanic/server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index e0bc4238..5cb57654 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -162,7 +162,10 @@ class HttpProtocol(asyncio.Protocol): self.write_error(exception) def on_url(self, url): - self.url = url + if not self.url: + self.url = url + else: + self.url += url def on_header(self, name, value): self._header_fragment += name From 2848d7c80ee2c8644b347666a34a0fb0114a7894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 16:44:57 +0200 Subject: [PATCH 08/18] Added a Forbidden exception Also added a small test. --- sanic/exceptions.py | 5 +++++ tests/test_exceptions.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e1136dd1..7a104122 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -194,6 +194,11 @@ class ContentRangeError(SanicException): } +@add_status_code(403) +class Forbidden(SanicException): + pass + + class InvalidRangeType(ContentRangeError): pass diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a2b8dc71..955a9ab4 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,7 +3,8 @@ from bs4 import BeautifulSoup from sanic import Sanic from sanic.response import text -from sanic.exceptions import InvalidUsage, ServerError, NotFound, abort +from sanic.exceptions import InvalidUsage, ServerError, NotFound, Forbidden +from sanic.exceptions import abort class SanicExceptionTestException(Exception): @@ -26,6 +27,10 @@ def exception_app(): def handler_404(request): raise NotFound("OK") + @app.route('/403') + def handler_403(request): + raise Forbidden("Forbidden") + @app.route('/invalid') def handler_invalid(request): raise InvalidUsage("OK") @@ -91,6 +96,12 @@ def test_not_found_exception(exception_app): assert response.status == 404 +def test_forbidden_exception(exception_app): + """Test the built-in Forbidden exception""" + request, response = exception_app.test_client.get('/403') + assert response.status == 403 + + def test_handled_unhandled_exception(exception_app): """Test that an exception not built into sanic is handled""" request, response = exception_app.test_client.get('/divide_by_zero') From dc5a70b0de2abdbb1943450112c7f7607068806a Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 26 Jun 2017 21:05:23 +0900 Subject: [PATCH 09/18] Introduce debug mode for HTTP protocol --- sanic/server.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 369e790e..fd31680e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol): signal=Signal(), connections=set(), request_timeout=60, request_max_size=None, request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, - state=None, **kwargs): + state=None, debug=False, **kwargs): self.loop = loop self.transport = None self.request = None @@ -102,12 +102,14 @@ class HttpProtocol(asyncio.Protocol): self.state = state if state else {} if 'requests_count' not in self.state: self.state['requests_count'] = 0 + self._debug = debug @property def keep_alive(self): - return (self._keep_alive - and not self.signal.stopped - and self.parser.should_keep_alive()) + return ( + self._keep_alive and + not self.signal.stopped and + self.parser.should_keep_alive()) # -------------------------------------------- # # Connection @@ -164,7 +166,10 @@ class HttpProtocol(asyncio.Protocol): try: self.parser.feed_data(data) except HttpParserError: - exception = InvalidUsage('Bad Request') + message = 'Bad Request' + if self._debug: + message += '\n' + traceback.format_exc() + exception = InvalidUsage(message) self.write_error(exception) def on_url(self, url): @@ -450,7 +455,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, router=router, websocket_max_size=websocket_max_size, websocket_max_queue=websocket_max_queue, - state=state + state=state, + debug=debug, ) server_coroutine = loop.create_server( From ad8e1cbf62db2f6d3b45e520dcb8a6c6b8933b3f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 20:49:41 -0700 Subject: [PATCH 10/18] convert environment vars to int if digits --- sanic/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index e3563bc1..b51f4d0c 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,4 +201,7 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - self[config_key] = v + if v.isdigit(): + self[config_key] = int(v) + else: + self[config_key] = v From 4379a4b0670c9172a3a6af63aa7d0132142c1989 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 20:58:31 -0700 Subject: [PATCH 11/18] float logic --- sanic/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index b51f4d0c..ec4f9bf3 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,7 +201,11 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - if v.isdigit(): - self[config_key] = int(v) + # This is a float or an int + if v.replace('.', '').isdigit(): + if '.' in v: + self[config_key] = float(v) + else: + self[config_key] = int(v) else: self[config_key] = v From 395d85a12f9b2be24dce112a6a4b0e873374de7f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 21:26:34 -0700 Subject: [PATCH 12/18] use try/except --- sanic/config.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index ec4f9bf3..f5649cfe 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,11 +201,10 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - # This is a float or an int - if v.replace('.', '').isdigit(): - if '.' in v: + try: + self[config_key] = int(v) + except ValueError: + try: self[config_key] = float(v) - else: - self[config_key] = int(v) - else: - self[config_key] = v + except ValueError: + self[config_key] = v From d2e14abfd56b041d6a5539dda40db717cdb3623b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Tue, 27 Jun 2017 12:57:47 +0200 Subject: [PATCH 13/18] Inverted the order of prefixes in Request.token property. As suggested by @allan-simon See: https://github.com/channelcat/sanic/pull/811#pullrequestreview-46144327 --- sanic/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index 29cb83f6..f1b3b441 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -86,7 +86,7 @@ class Request(dict): :return: token related to request """ - prefixes = ('Token ', 'Bearer ') + prefixes = ('Bearer', 'Token ') auth_header = self.headers.get('Authorization') if auth_header is not None: From 412ffd15923bad98d36d2bc344bf08656cacb303 Mon Sep 17 00:00:00 2001 From: Jonas Obrist Date: Wed, 28 Jun 2017 11:05:59 +0900 Subject: [PATCH 14/18] Added a warning to the cookies documentation about security --- docs/sanic/cookies.md | 75 ------------------------------------ docs/sanic/cookies.rst | 87 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 75 deletions(-) delete mode 100644 docs/sanic/cookies.md create mode 100644 docs/sanic/cookies.rst diff --git a/docs/sanic/cookies.md b/docs/sanic/cookies.md deleted file mode 100644 index e71bcc47..00000000 --- a/docs/sanic/cookies.md +++ /dev/null @@ -1,75 +0,0 @@ -# Cookies - -Cookies are pieces of data which persist inside a user's browser. Sanic can -both read and write cookies, which are stored as key-value pairs. - -## Reading cookies - -A user's cookies can be accessed via the `Request` object's `cookies` dictionary. - -```python -from sanic.response import text - -@app.route("/cookie") -async def test(request): - test_cookie = request.cookies.get('test') - return text("Test cookie set to: {}".format(test_cookie)) -``` - -## Writing cookies - -When returning a response, cookies can be set on the `Response` object. - -```python -from sanic.response import text - -@app.route("/cookie") -async def test(request): - response = text("There's a cookie up in this response") - response.cookies['test'] = 'It worked!' - response.cookies['test']['domain'] = '.gotta-go-fast.com' - response.cookies['test']['httponly'] = True - return response -``` - -## Deleting cookies - -Cookies can be removed semantically or explicitly. - -```python -from sanic.response import text - -@app.route("/cookie") -async def test(request): - response = text("Time to eat some cookies muahaha") - - # This cookie will be set to expire in 0 seconds - del response.cookies['kill_me'] - - # This cookie will self destruct in 5 seconds - response.cookies['short_life'] = 'Glad to be here' - response.cookies['short_life']['max-age'] = 5 - del response.cookies['favorite_color'] - - # This cookie will remain unchanged - response.cookies['favorite_color'] = 'blue' - response.cookies['favorite_color'] = 'pink' - del response.cookies['favorite_color'] - - return response -``` - -Response cookies can be set like dictionary values and have the following -parameters available: - -- `expires` (datetime): The time for the cookie to expire on the - client's browser. -- `path` (string): The subset of URLs to which this cookie applies. Defaults to /. -- `comment` (string): A comment (metadata). -- `domain` (string): Specifies the domain for which the cookie is valid. An - explicitly specified domain must always start with a dot. -- `max-age` (number): Number of seconds the cookie should live for. -- `secure` (boolean): Specifies whether the cookie will only be sent via - HTTPS. -- `httponly` (boolean): Specifies whether the cookie cannot be read by - Javascript. diff --git a/docs/sanic/cookies.rst b/docs/sanic/cookies.rst new file mode 100644 index 00000000..c4e0c0a1 --- /dev/null +++ b/docs/sanic/cookies.rst @@ -0,0 +1,87 @@ +Cookies +======= + +Cookies are pieces of data which persist inside a user's browser. Sanic can +both read and write cookies, which are stored as key-value pairs. + +.. warning:: + + Cookies can be freely altered by the client. Therefore you cannot just store + data such as login information in cookies as-is, as they can be freely altered + by the client. To ensure data you store in cookies is not forged or tampered + with by the client, use something like `itsdangerous`_ to cryptographically + sign the data. + + +Reading cookies +--------------- + +A user's cookies can be accessed via the ``Request`` object's ``cookies`` dictionary. + +.. code-block:: python + + from sanic.response import text + + @app.route("/cookie") + async def test(request): + test_cookie = request.cookies.get('test') + return text("Test cookie set to: {}".format(test_cookie)) + +Writing cookies +--------------- + +When returning a response, cookies can be set on the ``Response`` object. + +.. code-block:: python + + from sanic.response import text + + @app.route("/cookie") + async def test(request): + response = text("There's a cookie up in this response") + response.cookies['test'] = 'It worked!' + response.cookies['test']['domain'] = '.gotta-go-fast.com' + response.cookies['test']['httponly'] = True + return response + +Deleting cookies +---------------- + +Cookies can be removed semantically or explicitly. + +.. code-block:: python + + from sanic.response import text + + @app.route("/cookie") + async def test(request): + response = text("Time to eat some cookies muahaha") + + # This cookie will be set to expire in 0 seconds + del response.cookies['kill_me'] + + # This cookie will self destruct in 5 seconds + response.cookies['short_life'] = 'Glad to be here' + response.cookies['short_life']['max-age'] = 5 + del response.cookies['favorite_color'] + + # This cookie will remain unchanged + response.cookies['favorite_color'] = 'blue' + response.cookies['favorite_color'] = 'pink' + del response.cookies['favorite_color'] + + return response + +Response cookies can be set like dictionary values and have the following +parameters available: + +- ``expires`` (datetime): The time for the cookie to expire on the client's browser. +- ``path`` (string): The subset of URLs to which this cookie applies. Defaults to /. +- ``comment`` (string): A comment (metadata). +- ``domain`` (string): Specifies the domain for which the cookie is valid. An + explicitly specified domain must always start with a dot. +- ``max-age`` (number): Number of seconds the cookie should live for. +- ``secure`` (boolean): Specifies whether the cookie will only be sent via HTTPS. +- ``httponly`` (boolean): Specifies whether the cookie cannot be read by Javascript. + +.. _itsdangerous: https://pythonhosted.org/itsdangerous/ \ No newline at end of file From 1f24abc3d20f04135789852e9afce98a9e2f8edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Thu, 29 Jun 2017 10:23:49 +0200 Subject: [PATCH 15/18] Fixed support for "Bearer" and "Token" auth-schemes. Removed the test for "Authentication: Bearer Token " which was not supposed to exist (see https://github.com/channelcat/sanic/pull/821) Also added a call to `split` when retrieving the token value to handle cases where there are leading or trailing spaces. --- sanic/request.py | 4 ++-- tests/test_requests.py | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index f1b3b441..e21b8282 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -86,13 +86,13 @@ class Request(dict): :return: token related to request """ - prefixes = ('Bearer', 'Token ') + prefixes = ('Bearer', 'Token') auth_header = self.headers.get('Authorization') if auth_header is not None: for prefix in prefixes: if prefix in auth_header: - return auth_header.partition(prefix)[-1] + return auth_header.partition(prefix)[-1].strip() return auth_header diff --git a/tests/test_requests.py b/tests/test_requests.py index 671febeb..81fe1a5c 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -172,16 +172,6 @@ def test_token(): assert request.token == token - token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' - headers = { - 'content-type': 'application/json', - 'Authorization': 'Bearer Token {}'.format(token) - } - - request, response = app.test_client.get('/', headers=headers) - - assert request.token == token - token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' headers = { 'content-type': 'application/json', From e7c8035ed7bfc742083ddff88b0f92c3fb433348 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Thu, 29 Jun 2017 09:06:17 -0700 Subject: [PATCH 16/18] add pytest-sanic --- docs/sanic/extensions.md | 1 + docs/sanic/testing.md | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index da49da89..92b61f8c 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -23,3 +23,4 @@ A list of Sanic extensions created by the community. - [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic - [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation. - [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically. +- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously. diff --git a/docs/sanic/testing.md b/docs/sanic/testing.md index d4f61b4c..58c98858 100644 --- a/docs/sanic/testing.md +++ b/docs/sanic/testing.md @@ -57,3 +57,24 @@ def test_post_json_request_includes_data(): More information about the available arguments to aiohttp can be found [in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). + + +# pytest-sanic + +[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously. +Just write tests like, + +```python + async def test_sanic_db_find_by_id(app): + """ + Let's assume that, in db we have, + { + "id": "123", + "name": "Kobe Bryant", + "team": "Lakers", + } + """ + doc = await app.db["players"].find_by_id("123") + assert doc.name == "Kobe Bryant" + assert doc.team == "Lakers" +``` From fd5faeb5ddca1c831fa9bd91facbcc8f4cc4face Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Thu, 29 Jun 2017 09:14:21 -0700 Subject: [PATCH 17/18] add an example --- docs/sanic/testing.md | 71 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/docs/sanic/testing.md b/docs/sanic/testing.md index 58c98858..b8427a00 100644 --- a/docs/sanic/testing.md +++ b/docs/sanic/testing.md @@ -65,16 +65,63 @@ the available arguments to aiohttp can be found Just write tests like, ```python - async def test_sanic_db_find_by_id(app): - """ - Let's assume that, in db we have, - { - "id": "123", - "name": "Kobe Bryant", - "team": "Lakers", - } - """ - doc = await app.db["players"].find_by_id("123") - assert doc.name == "Kobe Bryant" - assert doc.team == "Lakers" +async def test_sanic_db_find_by_id(app): + """ + Let's assume that, in db we have, + { + "id": "123", + "name": "Kobe Bryant", + "team": "Lakers", + } + """ + doc = await app.db["players"].find_by_id("123") + assert doc.name == "Kobe Bryant" + assert doc.team == "Lakers" +``` + +[pytest-sanic](https://github.com/yunstanford/pytest-sanic) also provides some useful fixtures, like loop, unused_port, +test_server, test_client. + +```python +@pytest.yield_fixture +def app(): + app = Sanic("test_sanic_app") + + @app.route("/test_get", methods=['GET']) + async def test_get(request): + return response.json({"GET": True}) + + @app.route("/test_post", methods=['POST']) + async def test_post(request): + return response.json({"POST": True}) + + yield app + + +@pytest.fixture +def test_cli(loop, app, test_client): + return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol)) + + +######### +# Tests # +######### + +async def test_fixture_test_client_get(test_cli): + """ + GET request + """ + resp = await test_cli.get('/test_get') + assert resp.status == 200 + resp_json = await resp.json() + assert resp_json == {"GET": True} + +async def test_fixture_test_client_post(test_cli): + """ + POST request + """ + resp = await test_cli.post('/test_post') + assert resp.status == 200 + resp_json = await resp.json() + assert resp_json == {"POST": True} ``` From 021e9b228aacc84528451c73a7d602956d669504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 30 Jun 2017 16:16:20 +0200 Subject: [PATCH 18/18] Fixed a small error : `Sanic.__init__` doesn't have a `load_vars` parameter. It is `load_env`. --- docs/sanic/config.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index 2152a16c..ab63f7c8 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -31,10 +31,10 @@ There are several ways how to load configuration. ### From environment variables. -Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_vars` boolean to the Sanic constructor to override that: +Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_env` boolean to the Sanic constructor to override that: ```python -app = Sanic(load_vars=False) +app = Sanic(load_env=False) ``` ### From an Object