From 0464d31a9c91f70699b3ad5706f82927dc442623 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Sat, 10 Dec 2016 12:16:37 +0100 Subject: [PATCH 01/21] Find URL encoded filenames on the fs by decoding them first --- sanic/static.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index 72361a9a..ed7d6f8c 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -2,6 +2,7 @@ from aiofiles.os import stat from os import path from re import sub from time import strftime, gmtime +from urllib.parse import unquote from .exceptions import FileNotFound, InvalidUsage from .response import file, HTTPResponse @@ -38,6 +39,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # from herping a derp and treating the uri as an absolute path file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + + file_path = unquote(file_path) try: headers = {} # Check if the client has been sent this file before From 9ba2f99ea26c366aedea8f94ea0af152fcb43b99 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 01:10:24 +0100 Subject: [PATCH 02/21] added a comment on why to decode the file_path --- sanic/static.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index ed7d6f8c..b02786a4 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,6 +40,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + # URL decode the path sent by the browser otherwise we won't be able to + # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: headers = {} From 2003eceba19618fcb20d78b19af072726113cdfc Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 10:41:39 +0100 Subject: [PATCH 03/21] remove trailing space --- sanic/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index b02786a4..a70bff2f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,7 +40,7 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory - # URL decode the path sent by the browser otherwise we won't be able to + # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: From 75fc9f91b942f82cd9e25161e076afa28ae10472 Mon Sep 17 00:00:00 2001 From: 38elements Date: Sun, 18 Dec 2016 09:25:39 +0900 Subject: [PATCH 04/21] Change HttpParserError process --- sanic/server.py | 8 ++++---- tests/test_bad_request.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 tests/test_bad_request.py diff --git a/sanic/server.py b/sanic/server.py index a86da9fc..9340f374 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -14,7 +14,7 @@ except ImportError: from .log import log from .request import Request -from .exceptions import RequestTimeout, PayloadTooLarge +from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage class Signal: @@ -105,9 +105,9 @@ class HttpProtocol(asyncio.Protocol): # Parse request chunk or close connection try: self.parser.feed_data(data) - except HttpParserError as e: - self.bail_out( - "Invalid request data, connection closed ({})".format(e)) + except HttpParserError: + exception = InvalidUsage('Bad Request') + self.write_error(exception) def on_url(self, url): self.url = url diff --git a/tests/test_bad_request.py b/tests/test_bad_request.py new file mode 100644 index 00000000..095f4ab1 --- /dev/null +++ b/tests/test_bad_request.py @@ -0,0 +1,20 @@ +import asyncio +from sanic import Sanic + + +def test_bad_request_response(): + app = Sanic('test_bad_request_response') + lines = [] + async def _request(sanic, loop): + connect = asyncio.open_connection('127.0.0.1', 42101) + reader, writer = await connect + writer.write(b'not http') + while True: + line = await reader.readline() + if not line: + break + lines.append(line) + app.stop() + app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request) + assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n' + assert lines[-1] == b'Error: Bad Request' From 841125570095693a7fa8af5dbef3bfb62bf68dc5 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Fri, 23 Dec 2016 11:08:04 +1100 Subject: [PATCH 05/21] Create documentation for testing server endpoints. Currently the sanic.utils functionality is undocumented. This provides information on the interface as well as a complete example of testing a server endpoint. --- README.md | 1 + docs/testing.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/testing.md diff --git a/README.md b/README.md index 5aded700..e417b4a1 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ if __name__ == "__main__": * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) + * [Testing](docs/testing.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..79c719e8 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,51 @@ +# Testing + +Sanic endpoints can be tested locally using the `sanic.utils` module, which +depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/) +library. The `sanic_endpoint_test` function runs a local server, issues a +configurable request to an endpoint, and returns the result. It takes the +following arguments: + +- `app` An instance of a Sanic app. +- `method` *(default `'get'`)* A string representing the HTTP method to use. +- `uri` *(default `'/'`)* A string representing the endpoint to test. +- `gather_request` *(default `True`)* A boolean which determines whether the + original request will be returned by the function. If set to `True`, the + return value is a tuple of `(request, response)`, if `False` only the + response is returned. +- `loop` *(default `None`)* The event loop to use. +- `debug` *(default `False`)* A boolean which determines whether to run the + server in debug mode. + +The function further takes the `*request_args` and `**request_kwargs`, which +are passed directly to the aiohttp ClientSession request. For example, to +supply data with a GET request, `method` would be `get` and the keyword +argument `params={'value', 'key'}` would be supplied. 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). + +Below is a complete example of an endpoint test, +using [pytest](http://doc.pytest.org/en/latest/). The test checks that the +`/challenge` endpoint responds to a GET request with a supplied challenge +string. + +```python +import pytest +import aiohttp +from sanic.utils import sanic_endpoint_test + +# Import the Sanic app, usually created with Sanic(__name__) +from external_server import app + +def test_endpoint_challenge(): + # Create the challenge data + request_data = {'challenge': 'dummy_challenge'} + + # Send the request to the endpoint, using the default `get` method + request, response = sanic_endpoint_test(app, + uri='/challenge', + params=request_data) + + # Assert that the server responds with the challenge string + assert response.text == request_data['challenge'] +``` From 5c1ef2c1cfabcadbc7d4527d05d654e7ad23ab17 Mon Sep 17 00:00:00 2001 From: Romano Bodha Date: Fri, 23 Dec 2016 01:42:05 +0100 Subject: [PATCH 06/21] Fixed import error --- docs/class_based_views.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 223304ae..ee410b1d 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -6,6 +6,7 @@ Sanic has simple class based implementation. You should implement methods(get, p ```python from sanic import Sanic from sanic.views import HTTPMethodView +from sanic.response import text app = Sanic('some_name') From f091d82badc35116a45d59560f85cc6176980780 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 13:12:59 +0100 Subject: [PATCH 07/21] Improvement improvement: support fo binary data as a input. This do that the response process has more performance because not encoding needed. --- sanic/response.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 15130edd..1f86a807 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -79,7 +79,9 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body.encode('utf-8') + self.body = body + if type(body) is str: + self.body = body.encode('utf-8') else: self.body = body_bytes From 5afae986a0aeae6191a5532a11244cb7cc405f94 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 15:59:04 +0100 Subject: [PATCH 08/21] Apply response Middleware always Response middleware are useful to apply some post-process information, just before send to the user. For example: Add some HTTP headers (security headers, for example), remove "Server" banner (for security reasons) or cookie management. The change is very very simple: although an "request" middleware has produced any response, we'll even apply the response middlewares. --- sanic/sanic.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..f48b2bd5 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -193,18 +193,18 @@ class Sanic: if isawaitable(response): response = await response - # -------------------------------------------- # - # Response Middleware - # -------------------------------------------- # + # -------------------------------------------- # + # Response Middleware + # -------------------------------------------- # - if self.response_middleware: - for middleware in self.response_middleware: - _response = middleware(request, response) - if isawaitable(_response): - _response = await _response - if _response: - response = _response - break + if self.response_middleware: + for middleware in self.response_middleware: + _response = middleware(request, response) + if isawaitable(_response): + _response = await _response + if _response: + response = _response + break except Exception as e: # -------------------------------------------- # From 3add40625dc1b195c3129c34b88724e1a1869638 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 16:07:59 +0100 Subject: [PATCH 09/21] Explain how to chain two (or more) middlewares A funny and useful examples about how to chain middlewares. --- docs/middleware.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/middleware.md b/docs/middleware.md index 0b27443c..88d4e535 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -27,3 +27,23 @@ async def handler(request): app.run(host="0.0.0.0", port=8000) ``` + +## Middleware chain + +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: + +```python +app = Sanic(__name__) + +@app.middleware('response') +async def custom_banner(request, response): + response.headers["Server"] = "Fake-Server" + +@app.middleware('response') +async def prevent_xss(request, response): + response.headers["x-xss-protection"] = "1; mode=block" + +app.run(host="0.0.0.0", port=8000) +``` + +The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks. From cd17a42234c6465f70795f78436e0589badc5a25 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 23 Dec 2016 09:59:28 -0800 Subject: [PATCH 10/21] Fix some verbage --- docs/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/middleware.md b/docs/middleware.md index 88d4e535..39930e3e 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -30,7 +30,7 @@ app.run(host="0.0.0.0", port=8000) ## Middleware chain -If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware: ```python app = Sanic(__name__) From 32ea45d403ddb1e1b143ba2fbae0675f32adbc50 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 22 Dec 2016 21:00:57 -0800 Subject: [PATCH 11/21] allow overriding logging.basicConfig --- examples/override_logging.py | 23 +++++++++++++++++++++++ sanic/log.py | 2 -- sanic/sanic.py | 11 +++++++++-- tests/test_logging.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 examples/override_logging.py create mode 100644 tests/test_logging.py diff --git a/examples/override_logging.py b/examples/override_logging.py new file mode 100644 index 00000000..25fd78de --- /dev/null +++ b/examples/override_logging.py @@ -0,0 +1,23 @@ +from sanic import Sanic +from sanic.response import text +import json +import logging + +logging_format = "[%(asctime)s] %(process)d-%(levelname)s " +logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " +logging_format += "%(message)s" + +logging.basicConfig( + format=logging_format, + level=logging.DEBUG +) +log = logging.getLogger() + +# Set logger to override default basicConfig +sanic = Sanic(logger=True) +@sanic.route("/") +def test(request): + log.info("received request; responding with 'hey'") + return text("hey") + +sanic.run(host="0.0.0.0", port=8000) diff --git a/sanic/log.py b/sanic/log.py index bd2e499e..3988bf12 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,5 +1,3 @@ import logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") log = logging.getLogger(__name__) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..c79dca43 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -6,10 +6,11 @@ from multiprocessing import Process, Event from signal import signal, SIGTERM, SIGINT from time import sleep from traceback import format_exc +import logging from .config import Config from .exceptions import Handler -from .log import log, logging +from .log import log from .response import HTTPResponse from .router import Router from .server import serve @@ -18,7 +19,13 @@ from .exceptions import ServerError class Sanic: - def __init__(self, name=None, router=None, error_handler=None): + def __init__(self, name=None, router=None, + error_handler=None, logger=None): + if logger is None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..65de28c2 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,33 @@ +import asyncio +from sanic.response import text +from sanic import Sanic +from io import StringIO +from sanic.utils import sanic_endpoint_test +import logging + +logging_format = '''module: %(module)s; \ +function: %(funcName)s(); \ +message: %(message)s''' + +def test_log(): + log_stream = StringIO() + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + logging.basicConfig( + format=logging_format, + level=logging.DEBUG, + stream=log_stream + ) + log = logging.getLogger() + app = Sanic('test_logging', logger=True) + @app.route('/') + def handler(request): + log.info('hello world') + return text('hello') + + request, response = sanic_endpoint_test(app) + log_text = log_stream.getvalue().strip().split('\n')[-3] + assert log_text == "module: test_logging; function: handler(); message: hello world" + +if __name__ =="__main__": + test_log() From 2f0a582aa782f224e59911fa94d2f45c8c761e18 Mon Sep 17 00:00:00 2001 From: Konstantin Hantsov Date: Sat, 24 Dec 2016 10:28:34 +0100 Subject: [PATCH 12/21] Make golang performance test return JSON instead of string --- tests/performance/golang/golang.http.go | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/performance/golang/golang.http.go b/tests/performance/golang/golang.http.go index fb13cc8b..5aeedb61 100644 --- a/tests/performance/golang/golang.http.go +++ b/tests/performance/golang/golang.http.go @@ -1,16 +1,30 @@ package main import ( - "fmt" - "os" - "net/http" + "encoding/json" + "net/http" + "os" ) +type TestJSONResponse struct { + Test bool +} + func handler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) + response := TestJSONResponse{true} + + js, err := json.Marshal(response) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(js) } func main() { - http.HandleFunc("/", handler) - http.ListenAndServe(":" + os.Args[1], nil) + http.HandleFunc("/", handler) + http.ListenAndServe(":"+os.Args[1], nil) } From cc982c5a61ef9523e3565b9d3d014f404d4141ec Mon Sep 17 00:00:00 2001 From: cr0hn Date: Sat, 24 Dec 2016 15:24:25 +0100 Subject: [PATCH 13/21] Update response.py Type check by isinstance --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 1f86a807..c09c8dbd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -80,7 +80,7 @@ class HTTPResponse: if body is not None: self.body = body - if type(body) is str: + if isinstance(body, str): self.body = body.encode('utf-8') else: self.body = body_bytes From 74f305cfb78bc23e2dd647d83e26d658f29d1e19 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 14:06:53 -0800 Subject: [PATCH 14/21] Adds python36 to tox.ini and .travis.yml --- .travis.yml | 1 + tox.ini | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 942a5df2..5e41a68e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - '3.5' + - '3.6' install: - pip install -r requirements.txt - pip install -r requirements-dev.txt diff --git a/tox.ini b/tox.ini index 258395ed..ecb7ca87 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,30 @@ [tox] -envlist = py35, report +envlist = py35, py36 [testenv] deps = aiohttp pytest - # pytest-cov coverage commands = - coverage run -m pytest tests {posargs} + coverage run -m pytest -v tests {posargs} mv .coverage .coverage.{envname} -basepython: - py35: python3.5 - whitelist_externals = coverage mv echo +[testenv:flake8] +deps = + flake8 + +commands = + flake8 sanic + [testenv:report] commands = @@ -29,6 +32,3 @@ commands = coverage report coverage html echo "Open file://{toxinidir}/coverage/index.html" - -basepython = - python3.5 \ No newline at end of file From c2622511ce94f4a46599c23e6540128c65daacaf Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 12:20:16 -0800 Subject: [PATCH 15/21] Raise error if response is malformed. Issue #115 --- sanic/server.py | 6 ++++-- tests/test_requests.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 9340f374..11756005 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -6,6 +6,7 @@ from signal import SIGINT, SIGTERM from time import time from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError +from .exceptions import ServerError try: import uvloop as async_loop @@ -173,8 +174,9 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(e)) def bail_out(self, message): - log.debug(message) - self.transport.close() + exception = ServerError(message) + self.write_error(exception) + log.error(message) def cleanup(self): self.parser = None diff --git a/tests/test_requests.py b/tests/test_requests.py index 81895c8c..5895e3d5 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps from sanic import Sanic from sanic.response import json, text from sanic.utils import sanic_endpoint_test +from sanic.exceptions import ServerError # ------------------------------------------------------------ # @@ -32,6 +33,22 @@ def test_text(): assert response.text == 'Hello' +def test_invalid_response(): + app = Sanic('test_invalid_response') + + @app.exception(ServerError) + def handler_exception(request, exception): + return text('Internal Server Error.', 500) + + @app.route('/') + async def handler(request): + return 'This should fail' + + request, response = sanic_endpoint_test(app) + assert response.status == 500 + assert response.text == "Internal Server Error." + + def test_json(): app = Sanic('test_json') From 29f3c22fede7716cdebd06b8f4f44c48dfb0814e Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:12 -0800 Subject: [PATCH 16/21] Rework conditionals to not be inline --- sanic/static.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/static.py b/sanic/static.py index a70bff2f..e39dd76f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,12 +33,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - + # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path - file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ - if file_uri else file_or_directory + file_path = file_or_directory + if file_uri: + file_path = path.join( + file_or_directory, sub('^[/]*', '', file_uri)) # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) From 16182472fa73b6b5035ce4904bbb3edf3e1bf8a8 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:46 -0800 Subject: [PATCH 17/21] Remove trailing whitespace --- sanic/static.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index e39dd76f..9f5f2d52 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,7 +33,6 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path From 8be849cc40dc5f8f55536c462f9dff0c657f6a0f Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:16:19 -0800 Subject: [PATCH 18/21] Rewrite static files tests Relates to PR #188 Changes include: - Rewriting to work with pytest fixtures and an actual static directory - Addition of a test that covers file paths that must be unquoted as a uri --- tests/static/decode me.txt | 1 + tests/static/test.file | 1 + tests/test_static.py | 60 +++++++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 tests/static/decode me.txt create mode 100644 tests/static/test.file diff --git a/tests/static/decode me.txt b/tests/static/decode me.txt new file mode 100644 index 00000000..e5d05ac1 --- /dev/null +++ b/tests/static/decode me.txt @@ -0,0 +1 @@ +I need to be decoded as a uri diff --git a/tests/static/test.file b/tests/static/test.file new file mode 100644 index 00000000..0725a6ef --- /dev/null +++ b/tests/static/test.file @@ -0,0 +1 @@ +I am just a regular static file diff --git a/tests/test_static.py b/tests/test_static.py index 6dafac2b..82b0d1f9 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -1,30 +1,62 @@ import inspect import os +import pytest + from sanic import Sanic from sanic.utils import sanic_endpoint_test -def test_static_file(): - current_file = inspect.getfile(inspect.currentframe()) - with open(current_file, 'rb') as file: - current_file_contents = file.read() +@pytest.fixture(scope='module') +def static_file_directory(): + """The static directory to serve""" + current_file = inspect.getfile(inspect.currentframe()) + current_directory = os.path.dirname(os.path.abspath(current_file)) + static_directory = os.path.join(current_directory, 'static') + return static_directory + + +@pytest.fixture(scope='module') +def static_file_path(static_file_directory): + """The path to the static file that we want to serve""" + return os.path.join(static_file_directory, 'test.file') + + +@pytest.fixture(scope='module') +def static_file_content(static_file_path): + """The content of the static file to check""" + with open(static_file_path, 'rb') as file: + return file.read() + + +def test_static_file(static_file_path, static_file_content): app = Sanic('test_static') - app.static('/testing.file', current_file) + app.static('/testing.file', static_file_path) request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 - assert response.body == current_file_contents + assert response.body == static_file_content -def test_static_directory(): - current_file = inspect.getfile(inspect.currentframe()) - current_directory = os.path.dirname(os.path.abspath(current_file)) - with open(current_file, 'rb') as file: - current_file_contents = file.read() + +def test_static_directory( + static_file_directory, static_file_path, static_file_content): app = Sanic('test_static') - app.static('/dir', current_directory) + app.static('/dir', static_file_directory) - request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') + request, response = sanic_endpoint_test(app, uri='/dir/test.file') assert response.status == 200 - assert response.body == current_file_contents \ No newline at end of file + assert response.body == static_file_content + + +def test_static_url_decode_file(static_file_directory): + decode_me_path = os.path.join(static_file_directory, 'decode me.txt') + with open(decode_me_path, 'rb') as file: + decode_me_contents = file.read() + + app = Sanic('test_static') + app.static('/dir', static_file_directory) + + request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt') + assert response.status == 200 + assert response.body == decode_me_contents From d7e94473f3c350a1c3ec49e79f3a7e6c711b8842 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:37:55 -0800 Subject: [PATCH 19/21] Use a try/except, it's a bit faster Also reorder some imports and add some comments --- sanic/response.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index c09c8dbd..2c4c7f27 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,9 +1,11 @@ from aiofiles import open as open_async -from .cookies import CookieJar from mimetypes import guess_type from os import path + from ujson import dumps as json_dumps +from .cookies import CookieJar + COMMON_STATUS_CODES = { 200: b'OK', 400: b'Bad Request', @@ -79,9 +81,12 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body - if isinstance(body, str): + try: + # Try to encode it regularly self.body = body.encode('utf-8') + except AttributeError: + # Convert it to a str if you can't + self.body = str(body).encode('utf-8') else: self.body = body_bytes From f1f38c24da6d845701801c6f55ba9e5f24fd6acb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:47:15 -0800 Subject: [PATCH 20/21] Add test for PR: #215 --- tests/test_response.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_response.py diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 00000000..f35f10e9 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,18 @@ +from random import choice + +from sanic import Sanic +from sanic.response import HTTPResponse +from sanic.utils import sanic_endpoint_test + + +def test_response_body_not_a_string(): + """Test when a response body sent from the application is not a string""" + app = Sanic('response_body_not_a_string') + random_num = choice(range(1000)) + + @app.route('/hello') + async def hello_route(request): + return HTTPResponse(body=random_num) + + request, response = sanic_endpoint_test(app, uri='/hello') + assert response.text == str(random_num) From cf7616ebe5216007699cb264667079a3c739e29a Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:51:16 -0800 Subject: [PATCH 21/21] Increment version to 0.1.9 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 6e7f8d23..6b9b3a80 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.8' +__version__ = '0.1.9' __all__ = ['Sanic', 'Blueprint']