Merge branch 'master' into forbidden-exception
This commit is contained in:
commit
76e62779ba
|
@ -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.
|
|
87
docs/sanic/cookies.rst
Normal file
87
docs/sanic/cookies.rst
Normal file
|
@ -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/
|
|
@ -57,6 +57,12 @@ for Gunicorn `worker-class` argument:
|
||||||
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker
|
||||||
|
after it has processed a given number of requests. This can be a convenient way to help limit the effects
|
||||||
|
of the memory leak.
|
||||||
|
|
||||||
|
See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information.
|
||||||
|
|
||||||
## Asynchronous support
|
## Asynchronous support
|
||||||
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
|
This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`.
|
||||||
However be advised that this method does not support using multiple processes, and is not the preferred way
|
However be advised that this method does not support using multiple processes, and is not the preferred way
|
||||||
|
|
|
@ -201,4 +201,10 @@ class Config(dict):
|
||||||
for k, v in os.environ.items():
|
for k, v in os.environ.items():
|
||||||
if k.startswith(SANIC_PREFIX):
|
if k.startswith(SANIC_PREFIX):
|
||||||
_, config_key = k.split(SANIC_PREFIX, 1)
|
_, config_key = k.split(SANIC_PREFIX, 1)
|
||||||
self[config_key] = v
|
try:
|
||||||
|
self[config_key] = int(v)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
self[config_key] = float(v)
|
||||||
|
except ValueError:
|
||||||
|
self[config_key] = v
|
||||||
|
|
|
@ -203,6 +203,34 @@ class InvalidRangeType(ContentRangeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(401)
|
||||||
|
class Unauthorized(SanicException):
|
||||||
|
"""
|
||||||
|
Unauthorized exception (401 HTTP status code).
|
||||||
|
|
||||||
|
:param scheme: Name of the authentication scheme to be used.
|
||||||
|
:param realm: Description of the protected area. (optional)
|
||||||
|
:param challenge: A dict containing values to add to the WWW-Authenticate
|
||||||
|
header that is generated. This is especially useful when dealing with the
|
||||||
|
Digest scheme. (optional)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, message, scheme, realm="", challenge=None):
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
adds = ""
|
||||||
|
|
||||||
|
if challenge is not None:
|
||||||
|
values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()]
|
||||||
|
adds = ', '.join(values)
|
||||||
|
adds = ', {}'.format(adds)
|
||||||
|
|
||||||
|
self.headers = {
|
||||||
|
"WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def abort(status_code, message=None):
|
def abort(status_code, message=None):
|
||||||
"""
|
"""
|
||||||
Raise an exception based on SanicException. Returns the HTTP response
|
Raise an exception based on SanicException. Returns the HTTP response
|
||||||
|
|
|
@ -86,11 +86,15 @@ class Request(dict):
|
||||||
|
|
||||||
:return: token related to request
|
:return: token related to request
|
||||||
"""
|
"""
|
||||||
|
prefixes = ('Token ', 'Bearer ')
|
||||||
auth_header = self.headers.get('Authorization')
|
auth_header = self.headers.get('Authorization')
|
||||||
if auth_header is not None and 'Token ' in auth_header:
|
|
||||||
return auth_header.partition('Token ')[-1]
|
if auth_header is not None:
|
||||||
else:
|
for prefix in prefixes:
|
||||||
return auth_header
|
if prefix in auth_header:
|
||||||
|
return auth_header.partition(prefix)[-1]
|
||||||
|
|
||||||
|
return auth_header
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self):
|
def form(self):
|
||||||
|
|
|
@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
signal=Signal(), connections=set(), request_timeout=60,
|
signal=Signal(), connections=set(), request_timeout=60,
|
||||||
request_max_size=None, request_class=None, has_log=True,
|
request_max_size=None, request_class=None, has_log=True,
|
||||||
keep_alive=True, is_request_stream=False, router=None,
|
keep_alive=True, is_request_stream=False, router=None,
|
||||||
**kwargs):
|
state=None, debug=False, **kwargs):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.transport = None
|
self.transport = None
|
||||||
self.request = None
|
self.request = None
|
||||||
|
@ -99,12 +99,17 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self._request_handler_task = None
|
self._request_handler_task = None
|
||||||
self._request_stream_task = None
|
self._request_stream_task = None
|
||||||
self._keep_alive = keep_alive
|
self._keep_alive = keep_alive
|
||||||
|
self.state = state if state else {}
|
||||||
|
if 'requests_count' not in self.state:
|
||||||
|
self.state['requests_count'] = 0
|
||||||
|
self._debug = debug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keep_alive(self):
|
def keep_alive(self):
|
||||||
return (self._keep_alive
|
return (
|
||||||
and not self.signal.stopped
|
self._keep_alive and
|
||||||
and self.parser.should_keep_alive())
|
not self.signal.stopped and
|
||||||
|
self.parser.should_keep_alive())
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Connection
|
# Connection
|
||||||
|
@ -154,11 +159,17 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.headers = []
|
self.headers = []
|
||||||
self.parser = HttpRequestParser(self)
|
self.parser = HttpRequestParser(self)
|
||||||
|
|
||||||
|
# requests count
|
||||||
|
self.state['requests_count'] = self.state['requests_count'] + 1
|
||||||
|
|
||||||
# Parse request chunk or close connection
|
# Parse request chunk or close connection
|
||||||
try:
|
try:
|
||||||
self.parser.feed_data(data)
|
self.parser.feed_data(data)
|
||||||
except HttpParserError:
|
except HttpParserError:
|
||||||
exception = InvalidUsage('Bad Request')
|
message = 'Bad Request'
|
||||||
|
if self._debug:
|
||||||
|
message += '\n' + traceback.format_exc()
|
||||||
|
exception = InvalidUsage(message)
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
|
|
||||||
def on_url(self, url):
|
def on_url(self, url):
|
||||||
|
@ -389,7 +400,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
register_sys_signals=True, run_async=False, connections=None,
|
register_sys_signals=True, run_async=False, connections=None,
|
||||||
signal=Signal(), request_class=None, has_log=True, keep_alive=True,
|
signal=Signal(), request_class=None, has_log=True, keep_alive=True,
|
||||||
is_request_stream=False, router=None, websocket_max_size=None,
|
is_request_stream=False, router=None, websocket_max_size=None,
|
||||||
websocket_max_queue=None):
|
websocket_max_queue=None, state=None):
|
||||||
"""Start asynchronous HTTP Server on an individual process.
|
"""Start asynchronous HTTP Server on an individual process.
|
||||||
|
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
|
@ -427,8 +438,6 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
if debug:
|
if debug:
|
||||||
loop.set_debug(debug)
|
loop.set_debug(debug)
|
||||||
|
|
||||||
trigger_events(before_start, loop)
|
|
||||||
|
|
||||||
connections = connections if connections is not None else set()
|
connections = connections if connections is not None else set()
|
||||||
server = partial(
|
server = partial(
|
||||||
protocol,
|
protocol,
|
||||||
|
@ -445,7 +454,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
is_request_stream=is_request_stream,
|
is_request_stream=is_request_stream,
|
||||||
router=router,
|
router=router,
|
||||||
websocket_max_size=websocket_max_size,
|
websocket_max_size=websocket_max_size,
|
||||||
websocket_max_queue=websocket_max_queue
|
websocket_max_queue=websocket_max_queue,
|
||||||
|
state=state,
|
||||||
|
debug=debug,
|
||||||
)
|
)
|
||||||
|
|
||||||
server_coroutine = loop.create_server(
|
server_coroutine = loop.create_server(
|
||||||
|
@ -457,6 +468,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
sock=sock,
|
sock=sock,
|
||||||
backlog=backlog
|
backlog=backlog
|
||||||
)
|
)
|
||||||
|
|
||||||
# Instead of pulling time at the end of every request,
|
# Instead of pulling time at the end of every request,
|
||||||
# pull it once per minute
|
# pull it once per minute
|
||||||
loop.call_soon(partial(update_current_time, loop))
|
loop.call_soon(partial(update_current_time, loop))
|
||||||
|
@ -464,6 +476,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
if run_async:
|
if run_async:
|
||||||
return server_coroutine
|
return server_coroutine
|
||||||
|
|
||||||
|
trigger_events(before_start, loop)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -29,7 +29,7 @@ class GunicornWorker(base.Worker):
|
||||||
self.ssl_context = self._create_ssl_context(cfg)
|
self.ssl_context = self._create_ssl_context(cfg)
|
||||||
else:
|
else:
|
||||||
self.ssl_context = None
|
self.ssl_context = None
|
||||||
self.servers = []
|
self.servers = {}
|
||||||
self.connections = set()
|
self.connections = set()
|
||||||
self.exit_code = 0
|
self.exit_code = 0
|
||||||
self.signal = Signal()
|
self.signal = Signal()
|
||||||
|
@ -96,11 +96,16 @@ class GunicornWorker(base.Worker):
|
||||||
|
|
||||||
async def _run(self):
|
async def _run(self):
|
||||||
for sock in self.sockets:
|
for sock in self.sockets:
|
||||||
self.servers.append(await serve(
|
state = dict(requests_count=0)
|
||||||
|
self._server_settings["host"] = None
|
||||||
|
self._server_settings["port"] = None
|
||||||
|
server = await serve(
|
||||||
sock=sock,
|
sock=sock,
|
||||||
connections=self.connections,
|
connections=self.connections,
|
||||||
|
state=state,
|
||||||
**self._server_settings
|
**self._server_settings
|
||||||
))
|
)
|
||||||
|
self.servers[server] = state
|
||||||
|
|
||||||
async def _check_alive(self):
|
async def _check_alive(self):
|
||||||
# If our parent changed then we shut down.
|
# If our parent changed then we shut down.
|
||||||
|
@ -109,7 +114,15 @@ class GunicornWorker(base.Worker):
|
||||||
while self.alive:
|
while self.alive:
|
||||||
self.notify()
|
self.notify()
|
||||||
|
|
||||||
if pid == os.getpid() and self.ppid != os.getppid():
|
req_count = sum(
|
||||||
|
self.servers[srv]["requests_count"] for srv in self.servers
|
||||||
|
)
|
||||||
|
if self.max_requests and req_count > self.max_requests:
|
||||||
|
self.alive = False
|
||||||
|
self.log.info(
|
||||||
|
"Max requests exceeded, shutting down: %s", self
|
||||||
|
)
|
||||||
|
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self.log.info("Parent changed, shutting down: %s", self)
|
self.log.info("Parent changed, shutting down: %s", self)
|
||||||
else:
|
else:
|
||||||
|
@ -166,3 +179,4 @@ class GunicornWorker(base.Worker):
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self.exit_code = 1
|
self.exit_code = 1
|
||||||
self.cfg.worker_abort(self)
|
self.cfg.worker_abort(self)
|
||||||
|
sys.exit(1)
|
||||||
|
|
|
@ -3,8 +3,8 @@ from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Forbidden
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
||||||
from sanic.exceptions import abort
|
from sanic.exceptions import Forbidden, abort
|
||||||
|
|
||||||
|
|
||||||
class SanicExceptionTestException(Exception):
|
class SanicExceptionTestException(Exception):
|
||||||
|
@ -31,6 +31,20 @@ def exception_app():
|
||||||
def handler_403(request):
|
def handler_403(request):
|
||||||
raise Forbidden("Forbidden")
|
raise Forbidden("Forbidden")
|
||||||
|
|
||||||
|
@app.route('/401/basic')
|
||||||
|
def handler_401_basic(request):
|
||||||
|
raise Unauthorized("Unauthorized", "Basic", "Sanic")
|
||||||
|
|
||||||
|
@app.route('/401/digest')
|
||||||
|
def handler_401_digest(request):
|
||||||
|
challenge = {
|
||||||
|
"qop": "auth, auth-int",
|
||||||
|
"algorithm": "MD5",
|
||||||
|
"nonce": "abcdef",
|
||||||
|
"opaque": "zyxwvu",
|
||||||
|
}
|
||||||
|
raise Unauthorized("Unauthorized", "Digest", "Sanic", challenge)
|
||||||
|
|
||||||
@app.route('/invalid')
|
@app.route('/invalid')
|
||||||
def handler_invalid(request):
|
def handler_invalid(request):
|
||||||
raise InvalidUsage("OK")
|
raise InvalidUsage("OK")
|
||||||
|
@ -54,8 +68,10 @@ def exception_app():
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def test_catch_exception_list():
|
def test_catch_exception_list():
|
||||||
app = Sanic('exception_list')
|
app = Sanic('exception_list')
|
||||||
|
|
||||||
@app.exception([SanicExceptionTestException, NotFound])
|
@app.exception([SanicExceptionTestException, NotFound])
|
||||||
def exception_list(request, exception):
|
def exception_list(request, exception):
|
||||||
return text("ok")
|
return text("ok")
|
||||||
|
@ -101,6 +117,25 @@ def test_forbidden_exception(exception_app):
|
||||||
request, response = exception_app.test_client.get('/403')
|
request, response = exception_app.test_client.get('/403')
|
||||||
assert response.status == 403
|
assert response.status == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_unauthorized_exception(exception_app):
|
||||||
|
"""Test the built-in Unauthorized exception"""
|
||||||
|
request, response = exception_app.test_client.get('/401/basic')
|
||||||
|
assert response.status == 401
|
||||||
|
assert response.headers.get('WWW-Authenticate') is not None
|
||||||
|
assert response.headers.get('WWW-Authenticate') == "Basic realm='Sanic'"
|
||||||
|
|
||||||
|
request, response = exception_app.test_client.get('/401/digest')
|
||||||
|
assert response.status == 401
|
||||||
|
|
||||||
|
auth_header = response.headers.get('WWW-Authenticate')
|
||||||
|
assert auth_header is not None
|
||||||
|
assert auth_header.startswith('Digest')
|
||||||
|
assert "qop='auth, auth-int'" in auth_header
|
||||||
|
assert "algorithm='MD5'" in auth_header
|
||||||
|
assert "nonce='abcdef'" in auth_header
|
||||||
|
assert "opaque='zyxwvu'" in auth_header
|
||||||
|
|
||||||
|
|
||||||
def test_handled_unhandled_exception(exception_app):
|
def test_handled_unhandled_exception(exception_app):
|
||||||
"""Test that an exception not built into sanic is handled"""
|
"""Test that an exception not built into sanic is handled"""
|
||||||
|
|
|
@ -180,6 +180,16 @@ def test_token():
|
||||||
|
|
||||||
request, response = app.test_client.get('/', headers=headers)
|
request, response = app.test_client.get('/', headers=headers)
|
||||||
|
|
||||||
|
assert request.token == token
|
||||||
|
|
||||||
|
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'Authorization': 'Bearer {}'.format(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, response = app.test_client.get('/', headers=headers)
|
||||||
|
|
||||||
assert request.token == token
|
assert request.token == token
|
||||||
|
|
||||||
# no Authorization headers
|
# no Authorization headers
|
||||||
|
|
|
@ -3,7 +3,11 @@ import json
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from unittest import mock
|
||||||
|
from sanic.worker import GunicornWorker
|
||||||
|
from sanic.app import Sanic
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,3 +24,79 @@ def test_gunicorn_worker(gunicorn_worker):
|
||||||
with urllib.request.urlopen('http://localhost:1337/') as f:
|
with urllib.request.urlopen('http://localhost:1337/') as f:
|
||||||
res = json.loads(f.read(100).decode())
|
res = json.loads(f.read(100).decode())
|
||||||
assert res['test']
|
assert res['test']
|
||||||
|
|
||||||
|
|
||||||
|
class GunicornTestWorker(GunicornWorker):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.app = mock.Mock()
|
||||||
|
self.app.callable = Sanic("test_gunicorn_worker")
|
||||||
|
self.servers = {}
|
||||||
|
self.exit_code = 0
|
||||||
|
self.cfg = mock.Mock()
|
||||||
|
self.notify = mock.Mock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def worker():
|
||||||
|
return GunicornTestWorker()
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_init_process(worker):
|
||||||
|
with mock.patch('sanic.worker.asyncio') as mock_asyncio:
|
||||||
|
try:
|
||||||
|
worker.init_process()
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert mock_asyncio.get_event_loop.return_value.close.called
|
||||||
|
assert mock_asyncio.new_event_loop.called
|
||||||
|
assert mock_asyncio.set_event_loop.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_init_signals(worker):
|
||||||
|
worker.loop = mock.Mock()
|
||||||
|
worker.init_signals()
|
||||||
|
assert worker.loop.add_signal_handler.called
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_abort(worker):
|
||||||
|
with mock.patch('sanic.worker.sys') as mock_sys:
|
||||||
|
worker.handle_abort(object(), object())
|
||||||
|
assert not worker.alive
|
||||||
|
assert worker.exit_code == 1
|
||||||
|
mock_sys.exit.assert_called_with(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_quit(worker):
|
||||||
|
worker.handle_quit(object(), object())
|
||||||
|
assert not worker.alive
|
||||||
|
assert worker.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_max_requests_exceeded(worker):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
worker.ppid = 1
|
||||||
|
worker.alive = True
|
||||||
|
sock = mock.Mock()
|
||||||
|
sock.cfg_addr = ('localhost', 8080)
|
||||||
|
worker.sockets = [sock]
|
||||||
|
worker.wsgi = mock.Mock()
|
||||||
|
worker.connections = set()
|
||||||
|
worker.log = mock.Mock()
|
||||||
|
worker.loop = loop
|
||||||
|
worker.servers = {
|
||||||
|
"server1": {"requests_count": 14},
|
||||||
|
"server2": {"requests_count": 15},
|
||||||
|
}
|
||||||
|
worker.max_requests = 10
|
||||||
|
worker._run = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None))
|
||||||
|
|
||||||
|
# exceeding request count
|
||||||
|
_runner = asyncio.ensure_future(worker._check_alive(), loop=loop)
|
||||||
|
loop.run_until_complete(_runner)
|
||||||
|
|
||||||
|
assert worker.alive == False
|
||||||
|
worker.notify.assert_called_with()
|
||||||
|
worker.log.info.assert_called_with("Max requests exceeded, shutting down: %s",
|
||||||
|
worker)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user