sanic-org / sanic-testing

Test clients for Sanic

Home Page:https://sanic.dev/en/plugins/sanic-testing/getting-started.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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?

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.