Testing CORS fails
SerGeRybakov opened this issue · comments
Hi there!
I've implemented sanic-extensions and deleted all my previous CORS-code, that was inspired by these instructions.
So for now I have such code.
"""API application."""
import random
import string
from sanic import Sanic
from sanic.exceptions import ServerError, NotFound, InvalidUsage, MethodNotSupported
from sanic_ext import Extend
from api import api_blueprints, IndexRoute
from api.middlewares.on_request import validate_request
from api.middlewares.on_response import send_metrics
from api.middlewares.on_start import create_app_context
from cfg import config
from tools.abstract.interfaces import BaseView
def create_app(
*, api_config: object = config.APIConfig, log_config: dict = config.logging_config
) -> Sanic:
"""Create and return Sanic application."""
app = Sanic(name=random.choice(string.ascii_letters), log_config=log_config)
app.update_config(api_config)
app.config["API_VERSION"] = config.version
cors_headers = "origin, content-type, accept, authorization, x-xsrf-token, x-request-id"
app.config.CORS_ORIGINS = "*"
app.config.CORS_METHODS = "*"
app.config.CORS_ALLOW_HEADERS = cors_headers
app.config.CORS_EXPOSE_HEADERS = cors_headers
Extend(app)
app.listener(create_app_context, "before_server_start")
app.middleware(validate_request, "request")
app.middleware(send_metrics, "response")
app.blueprint(api_blueprints)
app.add_route(IndexRoute.as_view(), "/", name="index")
app.error_handler.add(NotFound, BaseView.not_found)
app.error_handler.add(InvalidUsage, BaseView.bad_request)
app.error_handler.add(ServerError, BaseView.server_error)
app.error_handler.add(MethodNotSupported, BaseView.method_not_allowed)
app.error_handler.add(Exception, BaseView.server_error)
app.ctx.config = config
return app
if __name__ == "__main__":
api = create_app()
api.run(debug=True)
My BaseView class looks like this:
class BaseView(HTTPMethodView):
"""Base class-based view providing all HTTP-methods."""
@classmethod
async def ok_response(cls, request: Request, response: Union[str, int, float, Iterable, dict]):
"""Return OK and message."""
code = 200
if request.method == "POST":
code = 201
if isinstance(response, str):
return json({"code": code, "message": response}, status=code)
return json(response, code)
@classmethod
async def bad_request(cls, request: Request, exception=None):
"""Return 400 "Bad Request" and message."""
if exception:
logger.exception(f"Code 400: {exception}")
return json({"code": 400, "message": f"Bad request: {exception}"}, status=400)
return json({"code": 400, "message": f"Bad request: {await get_request_info(request)}"}, status=400)
@classmethod
async def not_found(cls, request: Request, exception=None):
"""Return 404 "Not Found" and message."""
if exception:
logger.exception(f"Code 404: {exception}")
return json({"code": 404, "message": f"Not found: {exception}"}, status=404)
return json({"code": 404, "message": f"Not found: {await get_request_info(request)}"}, status=404)
@classmethod
async def method_not_allowed(cls, request: Request, exception=None):
"""Return 405 "Method Not Allowed" and message."""
return json({"code": 405, "message": f"Method not allowed: {request.method} {request.url}"}, status=405)
@classmethod
async def server_error(cls, request: Request, exception):
"""Return 500 "Internal Server Error" and message."""
logger.exception(f"Code 500: {exception}")
request_info = await get_request_info(request)
message = exception.args[0]
return json({"code": 500, "exception": message, "request": request_info}, status=500)
@openapi.exclude(True)
async def get(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def post(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def put(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def patch(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def delete(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
async def head(self, request: Request, *args, **kwargs):
return empty()
async def options(self, request: Request, *args, **kwargs):
return empty()
And my IndexView looks like this:
class IndexRoute(BaseView):
"""Base index route."""
async def get(self, request: Request):
"""Redirects to API swagger documentation."""
return redirect(request.app.url_for("openapi.swagger"))
So, after implementing Extend(app) I get
serge@serge:~$ curl -X HEAD http://0.0.0.0:8000/ -I
HTTP/1.1 204 No Content
access-control-allow-origin: *
access-control-expose-headers: content-type, authorization,origin, accept, x-xsrf-token, x-request-id
connection: keep-alive
But when I'm using sanic-testing for testing HEAD and OPTIONS methods my tests fail. Moreover any other test that was successfully passed (no AssertionError) has a traceback with SanicExceptionError.
Let's go step by step.
My client-fixture and HEAD-test code:
@pytest.fixture()
def app_client() -> SanicASGITestClient:
"""Sanic test asgi-client."""
app: Sanic = create_app()
manager: TestManager = TestManager(app)
return manager.asgi_client
@pytest.mark.asyncio()
class TestIndex:
url = "/"
async def test_index_200(self, app_client: SanicASGITestClient):
"""Test index route opens swagger docs route."""
req, resp = await app_client.get(self.url)
assert req.app.url_for("openapi.swagger") in req.url
assert resp.status == 200
print(app_client.sanic_app.ctx.config.mongo.triggers_coll)
async def test_index_302(self, app_client: SanicASGITestClient):
"""Test index route is actually redirecting to swagger docs route."""
req, resp = await app_client.get(self.url, allow_redirects=False)
assert req.app.url_for("openapi.swagger") not in req.url
assert resp.status == 302
async def test_head(self, app_client: SanicASGITestClient):
"""Check CORS is enabled."""
req, res = await app_client.head(self.url)
try:
assert res.status == 204
assert res.headers == {
"access-control-allow-origin": "*",
"access-control-expose-headers": "origin, content-type, accept, authorization, x-xsrf-token, x-request-id",
}
finally:
print(res.headers)
The last test fails with traceback:
FAILED [100%][sanic.root INFO [2021-12-09 15:52:23 +0300]]: HEAD /? : 0.00017905235290527344 ms
Headers({})
[sanic.error ERROR [2021-12-09 15:52:23 +0300]]: Exception occurred in one of response middleware handlers
Traceback (most recent call last):
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic/request.py", line 187, in respond
response = await self.app._run_response_middleware(
File "_run_response_middleware", line 22, in _run_response_middleware
from ssl import Purpose, SSLContext, create_default_context
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 54, in _add_cors_headers
_add_origin_header(request, response)
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 160, in _add_origin_header
allow_origins = _get_from_cors_ctx(
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 151, in _get_from_cors_ctx
value = getattr(request.route.ctx._cors, key, default)
AttributeError: 'types.SimpleNamespace' object has no attribute '_cors'
tests/test_api/test_index.py:23 (TestIndex.test_head)
Headers({}) != {'access-control-allow-origin': '*', 'access-control-expose-headers': 'origin, content-type, accept, authorization, x-xsrf-token, x-request-id'}
Expected :{'access-control-allow-origin': '*', 'access-control-expose-headers': 'origin, content-type, accept, authorization, x-xsrf-token, x-request-id'}
Actual :Headers({})
And the first two cases pass, but have the following trace:
PASSED [ 33%][sanic.root INFO [2021-12-09 15:52:23 +0300]]: GET /? : 0.00021386146545410156 ms
[sanic.root INFO [2021-12-09 15:52:23 +0300]]: GET /docs/swagger? : 0.0001621246337890625 ms
test_triggers
[sanic.error ERROR [2021-12-09 15:52:23 +0300]]: Exception occurred in one of response middleware handlers
Traceback (most recent call last):
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic/request.py", line 187, in respond
response = await self.app._run_response_middleware(
File "_run_response_middleware", line 22, in _run_response_middleware
from ssl import Purpose, SSLContext, create_default_context
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 54, in _add_cors_headers
_add_origin_header(request, response)
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 160, in _add_origin_header
allow_origins = _get_from_cors_ctx(
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 151, in _get_from_cors_ctx
value = getattr(request.route.ctx._cors, key, default)
AttributeError: 'types.SimpleNamespace' object has no attribute '_cors'
[sanic.error ERROR [2021-12-09 15:52:23 +0300]]: Exception occurred in one of response middleware handlers
Traceback (most recent call last):
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic/request.py", line 187, in respond
response = await self.app._run_response_middleware(
File "_run_response_middleware", line 22, in _run_response_middleware
from ssl import Purpose, SSLContext, create_default_context
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 54, in _add_cors_headers
_add_origin_header(request, response)
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 160, in _add_origin_header
allow_origins = _get_from_cors_ctx(
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 151, in _get_from_cors_ctx
value = getattr(request.route.ctx._cors, key, default)
AttributeError: 'types.SimpleNamespace' object has no attribute '_cors'
I wonder is that my fault or it is a bug in testing module? Will be glad to answer your questions to resolve this issue.
By the way, when we may expect the sanic-testing documentation? It was written that it shall be published in October, 2021, but still there is no any.
P.S.: My environment is:
python 3.10.1
sanic 21.9.3
sanic-ext 21.9.3
sanic-testing 0.7.0
sanic-routing 0.7.2
Hmm.... I am curious to hear if this happens with SanicTestClient
(not the ASGI client.
Without looking deeply, my instinct tells me that something it not attaching at startup time properly. There is an operation that is supposed to happen on server startup, it seems that is not running.
Well, even if I comment out all CORS lines in create_app and insert them into my client-fixture,
@pytest.fixture()
def app_client() -> SanicASGITestClient:
"""Sanic test asgi-client."""
app: Sanic = create_app()
cors_headers = "origin, content-type, accept, authorization, x-xsrf-token, x-request-id"
app.config.CORS_ORIGINS = "*"
app.config.CORS_METHODS = "*"
app.config.CORS_ALLOW_HEADERS = cors_headers
app.config.CORS_EXPOSE_HEADERS = cors_headers
Extend(app)
manager: TestManager = TestManager(app)
return manager.asgi_client
I'll receive totally the same effect.
I get that. My point was that they do not actually evaluate and attach until before_server_start. Which, perhaps that is a bug that this listener is not firing on the ASGI testing client. I know it does with the regular client.
A quick look at the code (from my phone) confirms this suspicion. This is a bug with the ASGIClient I believe.
May be the grounds of this problem are similar to this issue?
Yup. will implement and release a fix before the next release (end of this month)
Excuse me, I didn't get.
You closed issue because everything is fixed now?
yes
Well, I don't have any effect then.
I reinstalled sanic-testing several times with pip and by pointing the git branch and commit hash - no effect, although the committed changee were installed.