GithubHelp home page GithubHelp logo

sanic-org / sanic-testing Goto Github PK

View Code? Open in Web Editor NEW
31.0 31.0 18.0 104 KB

Test clients for Sanic

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

License: MIT License

Python 99.39% Makefile 0.61%
hacktoberfest python sanic testing

sanic-testing's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

sanic-testing's Issues

How to implement 'before_server_start' listener in an async test

ERROR    sanic.error:handlers.py:146 Exception occurred while handling uri: 'http://mockserver:1234/api/v3/projects/fourth-memento-308807-test2/locations/cn/agents'
Traceback (most recent call last):
  File "xx/.venv/lib/python3.8/site-packages/sanic/app.py", line 749, in handle_request
    response = await self._run_request_middleware(
  File "xx/.venv/lib/python3.8/site-packages/sanic/app.py", line 1116, in _run_request_middleware
    response = await response
  File "xx/tests/api/base.py", line 68, in load_request_ctx
    request.ctx.engine = AIOEngine(request.app.ctx.mongo_client, database=request.ctx.database_name)
AttributeError: 'types.SimpleNamespace' object has no attribute 'mongo_client'
=============================== warnings summary ===============================
tests/api/test_agent.py::test_agent_list_get
  xx/.venv/lib/python3.8/site-packages/sanic/asgi.py:22: UserWarning: You have set a listener for "before_server_start" in ASGI mode. It will be executed as early as possible, but not before the ASGI server is started.
    warnings.warn(

When I used async test, I encountered the above error, but there was no problem when it was sync test.
I have a before_server_start listener to add mongo motor.

Unable to install sanic_testing and sanic in Python 3.6

Hi,

It looks like sanic and sanic-testing can't be co-installed on Python 3.6, I assume because sanic 20.12 requires httpx==0.15.4 but there's no version of sanic-testing that supports a version that old?

I can apply the obvious workaround to either stop using such an old version of Python, or use "sanic_testing; python_version > '3.6'", and some conditional importing to make it work, but it might be worth dropping the 3.6 classifier from https://github.com/sanic-org/sanic-testing/blob/main/setup.py#L44 to make it more obvious it isn't going to work?

Cheers!

python3 --version
Python 3.6.12python3 -m venv testvenvsource testvenv/bin/activatepip install -U pip
Looking in indexes: https://pypi.org/simple, https://artifactory.virt.ch.bbc.co.uk/artifactory/api/pypi/ap-python/simple
Collecting pip
  Using cached https://files.pythonhosted.org/packages/ca/31/b88ef447d595963c01060998cb329251648acf4a067721b0452c45527eb8/pip-21.2.4-py3-none-any.whl
Installing collected packages: pip
  Found existing installation: pip 18.1
    Uninstalling pip-18.1:
      Successfully uninstalled pip-18.1
Successfully installed pip-21.2.4
pip install sanic sanic-testing
Looking in indexes: https://pypi.org/simple, https://artifactory.virt.ch.bbc.co.uk/artifactory/api/pypi/ap-python/simple
Collecting sanic
  Using cached sanic-20.12.3-py3-none-any.whl (80 kB)
Collecting sanic-testing
  Using cached sanic_testing-0.6.0-py3-none-any.whl (7.2 kB)
Collecting uvloop<0.15.0,>=0.5.3
  Using cached uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl (1.5 MB)
Collecting httptools>=0.0.10
  Using cached httptools-0.3.0-cp36-cp36m-macosx_10_9_x86_64.whl (155 kB)
Collecting ujson>=1.35
  Using cached ujson-4.1.0-cp36-cp36m-macosx_10_14_x86_64.whl (45 kB)
Collecting aiofiles>=0.6.0
  Using cached aiofiles-0.7.0-py3-none-any.whl (13 kB)
Collecting httpx==0.15.4
  Using cached httpx-0.15.4-py3-none-any.whl (65 kB)
Collecting multidict<6.0,>=5.0
  Using cached multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl (49 kB)
Collecting websockets<9.0,>=8.1
  Using cached websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl (66 kB)
Collecting httpcore==0.11.*
  Using cached httpcore-0.11.1-py3-none-any.whl (52 kB)
Collecting rfc3986[idna2008]<2,>=1.3
  Using cached rfc3986-1.5.0-py2.py3-none-any.whl (31 kB)
Collecting sniffio
  Using cached sniffio-1.2.0-py3-none-any.whl (10 kB)
Collecting certifi
  Using cached certifi-2021.5.30-py2.py3-none-any.whl (145 kB)
Collecting h11<0.10,>=0.8
  Using cached h11-0.9.0-py2.py3-none-any.whl (53 kB)
Collecting contextvars>=2.1
  Using cached contextvars-2.4-py3-none-any.whl
Collecting sanic-testing
  Using cached sanic_testing-0.4.0-py3-none-any.whl (7.1 kB)
  Using cached sanic_testing-0.3.1-py3-none-any.whl (6.3 kB)
  Using cached sanic_testing-0.3.0-py3-none-any.whl (6.3 kB)
  Using cached sanic_testing-0.2.1-py3-none-any.whl (6.0 kB)
  Using cached sanic_testing-0.2.0-py3-none-any.whl (6.0 kB)
  Using cached sanic_testing-0.1.2-py3-none-any.whl (5.7 kB)
  Using cached sanic_testing-0.1.1-py3-none-any.whl (5.6 kB)
  Using cached sanic_testing-0.1.0-py3-none-any.whl (5.5 kB)
INFO: pip is looking at multiple versions of sniffio to determine which version is compatible with other requirements. This could take a while.
Collecting sniffio
  Using cached sniffio-1.1.0-py3-none-any.whl (4.5 kB)
  Downloading sniffio-1.0.0-py3-none-any.whl (4.4 kB)
INFO: pip is looking at multiple versions of httpcore to determine which version is compatible with other requirements. This could take a while.
Collecting httpcore==0.11.*
  Downloading httpcore-0.11.0-py3-none-any.whl (52 kB)
     |████████████████████████████████| 52 kB 628 kB/s 
INFO: pip is looking at multiple versions of httpx to determine which version is compatible with other requirements. This could take a while.
INFO: pip is looking at multiple versions of <Python from Requires-Python> to determine which version is compatible with other requirements. This could take a while.
INFO: pip is looking at multiple versions of sanic to determine which version is compatible with other requirements. This could take a while.
Collecting sanic
  Using cached sanic-20.12.2-py3-none-any.whl (79 kB)
INFO: pip is looking at multiple versions of sniffio to determine which version is compatible with other requirements. This could take a while.
  Using cached sanic-20.12.1-py3-none-any.whl (79 kB)
INFO: This is taking longer than usual. You might need to provide the dependency resolver with stricter constraints to reduce runtime. If you want to abort this run, you can press Ctrl + C to do so. To improve how pip performs, tell us what happened here: https://pip.pypa.io/surveys/backtracking
  Using cached sanic-20.12.0-py3-none-any.whl (79 kB)
INFO: pip is looking at multiple versions of httpcore to determine which version is compatible with other requirements. This could take a while.
<snip very long output - gave up after 5min>

Sanic app without any endpoints fails during routing

import pytest
from sanic import Sanic
from sanic_testing import TestManager


@pytest.fixture
def app():
    sanic_app = Sanic(__name__)
    TestManager(sanic_app)

    return sanic_app


@pytest.mark.asyncio
async def test_not_found(app):
    request, response = await app.asgi_client.get("/not-a-known-endpoint")

    assert response.status == 404

This test will fail with an exception related to routing when ran.

Can't run multiple tests with same instance of ReusableClient

How to reproduce

import pytest
from sanic_testing.reusable import ReusableClient
from sanic import Sanic
from sanic import response


@pytest.fixture(scope="session")
def app():
    sanic_app = Sanic("TestSanic")

    @sanic_app.get("/")
    def basic(request):
        return response.text("foo")

    @sanic_app.post("/api/login")
    def basic(request):
        return response.text("foo")

    @sanic_app.get("/api/resources")
    def basic(request):
        return response.text("foo")

    return sanic_app


@pytest.fixture(scope="session")
def cli(app):
    cli = ReusableClient(app)
    return cli


def test_root(cli):
    with cli:
        _, response = cli.get("/")
        assert response.status == 200


def test_login(cli):
    with cli:
        _, response = cli.post("/api/login", )
        assert response.status == 200

        _, response = cli.get("/api/resources")
        assert response.status == 200

Error that I got

self = <_UnixSelectorEventLoop running=False closed=False debug=False>
future = <Task finished name='Task-18' coro=<StartupMixin.create_server() done, defined at /home/ghost/wuw2/lib/python3.10/site-packages/sanic/mixins/startup.py:347> exception=RuntimeError('cannot reuse already awaited coroutine')>

    def run_until_complete(self, future):
        """Run until the Future is done.
    
        If the argument is a coroutine, it is wrapped in a Task.
    
        WARNING: It would be disastrous to call run_until_complete()
        with the same coroutine twice -- it would wrap it in two
        different Tasks and that can't be good.
    
        Return the Future's result, or raise its exception.
        """
        self._check_closed()
        self._check_running()
    
        new_task = not futures.isfuture(future)
        future = tasks.ensure_future(future, loop=self)
        if new_task:
            # An exception is raised if the future didn't complete, so there
            # is no need to log the "destroy pending task" message
            future._log_destroy_pending = False
    
        future.add_done_callback(_run_until_complete_cb)
        try:
            self.run_forever()
        except:
            if new_task and future.done() and not future.cancelled():
                # The coroutine raised a BaseException. Consume the exception
                # to not log a warning, the caller doesn't have access to the
                # local task.
                future.exception()
            raise
        finally:
            future.remove_done_callback(_run_until_complete_cb)
        if not future.done():
            raise RuntimeError('Event loop stopped before Future completed.')
    
>       return future.result()
E       RuntimeError: cannot reuse already awaited coroutine

KeyError when used multiple apps and ReusableClient

Error

[2022-11-02 16:00:05 +0000] [82325] [ERROR] Experienced exception while trying to serve
Traceback (most recent call last):
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/mixins/startup.py", line 921, in serve_single
    worker_serve(monitor_publisher=None, **kwargs)
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/worker/serve.py", line 106, in worker_serve
    return _serve_http_1(
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/server/runners.py", line 231, in _serve_http_1
    loop.run_until_complete(app._server_event("init", "before"))
  File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/app.py", line 1549, in _server_event
    await self.dispatch(
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/signals.py", line 197, in dispatch
    return await dispatch
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/signals.py", line 167, in _dispatch
    retval = await maybe_coroutine
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/app.py", line 1140, in _listener
    await maybe_coro
  File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/mixins/startup.py", line 1056, in _start_servers
    if not server_info.settings["loop"]:
KeyError: 'loop'

How to reproduce

  1. Create a new env with the following dependencies:
sanic = "22.9.0"
pytest = "^7.2.0"
sanic-testing = "==22.9.0"
  1. Create a test file like this:
import pytest
from sanic import Sanic, response
from sanic_testing.reusable import ReusableClient


@pytest.fixture
def app():
    sanic_app = Sanic("app")

    @sanic_app.get("/")
    def basic(request):
        return response.text("foo")

    yield sanic_app


@pytest.fixture
def client(app):
    client = ReusableClient(app, port=9999)
    client.run()
    yield client
    client.stop()


@pytest.fixture()
def app_2():
    app = Sanic("app_2")

    @app.route("/")
    def handler(request):
        return response.text("OK")

    yield app

def test_example_1(client):
    _, response = client.get("/")

    assert response.body == b"foo"
    assert response.status == 200


def test_example_2(app_2):
    _, response = app_2.test_client.get("/")

    assert response.body == b"OK"
    assert response.status == 200

Notes

I'm not familiar with the code but it looks like a bug in ReusableClient - it creates ApplicationServerInfo with the default settings dictionary which misses the loop key there.

The tests pass if I change that to:

self.app.state.server_info.append(
    ApplicationServerInfo(
        settings={
            "version": "1.1",
            "ssl": None,
            "unix": None,
            "sock": None,
            "host": self.host,
            "port": self.port,
            "loop": None,
        }
    )
)

Allow httpx 0.24

httpx 0.24.0 has been released (changelog) so it would be nice to allow sanic-testing to use the latest version if possible

CI

We need to setup CI. Maybe try GitHub Actions for this?

TypeError: _blank() takes 1 positional

I am having this error:

    def _run_request(self, *args, **kwargs):
>       return self._do_request(*args, **kwargs)
E       TypeError: _blank() takes 1 positional argument but 2 were given

/usr/local/lib/python3.10/site-packages/sanic_testing/testing.py:68: TypeError

Test function:


@pytest.mark.asyncio()
async def test_index(test_cli, app):
    resp = await test_cli.get('/')
    assert resp.status_code == 200

Versions:

pytest-sanic==1.9.1
sanic==21.6.2
sanic-babel==0.3.0
sanic-base-extension==0.1.1
sanic-devtools==0.1.0
sanic-jwt==1.7.0
sanic-routing==0.7.2
sanic-testing==0.7.0

This fixed it for me:
pawelkoston@9bb5842

How to create test databases and connect them to the test Sanic app?

I am trying to test app with TortoiseORM and i dont understand what is the right way to setup my test application in fixture.

I was tried to make with listeners and fixtures but i am always how problems with event loop, or tortoise can't connect to the database, or fixture doesnt work and etc. And i can't find any example on stackoverflow/github with testing Sanic with tortoise and test databases.

Can you show me the best way to setup tests for testing Sanic & TortoiseORM on the test db (e.g. sqlite) ?

p.s I think i can use sqlite:///:memory for

JSON Payload Sometimes Missing From Request

Hi,

I took a go at switching over to sanic-testing for my unit tests on my Sanic project, and noticed that some of my unit tests where inconsistently failing after migrating them from pytest-sanic, which I was using before.

It seems that some of my tests that were passing a json payload were sometimes turning up in the request handlers without any payload on the request.

I've produced what looks to be minimal reproduction of this bug. Removing any of this (the unused GET handler, or the blueprint) seem to stop the error somewhat reliably popping up for me, so it appears they are related.

I've made a public GitHub repo here with this example, but also including the setup below, if that's easier.

Here's the test case:

import pytest

from sanic import Sanic
from sanic import Blueprint
from sanic.request import Request
from sanic.response import text
from sanic.exceptions import InvalidUsage

from sanic_testing import TestManager


@pytest.fixture
def app():
    app = Sanic(__name__)

    rest = Blueprint("rest", url_prefix="/rest")

    @rest.get("/users")
    async def select(request: Request, user: dict):
        pass

    @rest.put("/users")
    async def update(request: Request):
        if request.json is None:
            raise InvalidUsage("Missing json payload")
        else:
            return text("ok")

    app.blueprint(rest)

    return TestManager(app)


def test_json_payload(app):
    request, response = app.test_client.put("/rest/users", json={})
    assert response.status == 200

On running pytest I will get (inconsistently) either a pass or fail, where sometimes the json payload available on request.json and other times it is set to None. Here's the output of two consecutive runs from my terminal:

$ pipenv run pytest tests
Loading .env environment variables...
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.2, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: /private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys
collected 1 item                                                                                                                                      

tests/test_main.py F                                                                                                                            [100%]

====================================================================== FAILURES =======================================================================
__________________________________________________________________ test_json_payload __________________________________________________________________

app = <sanic_testing.manager.TestManager object at 0x10e2927c0>

    def test_json_payload(app):
        request, response = app.test_client.put("/rest/users", json={})
>       assert response.status == 200
E       assert 400 == 200
E        +  where 400 = <Response [400 Bad Request]>.status

tests/test_main.py:36: AssertionError
---------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------
[2021-03-31 19:50:14 +0100] [62919] [INFO] http://127.0.0.1:52851/rest/users
[2021-03-31 19:50:14 +0100] - (sanic.access)[INFO][127.0.0.1:52852]: PUT http://127.0.0.1:52851/rest/users  400 -1
[2021-03-31 19:50:14 +0100] [62919] [INFO] Starting worker [62919]
[2021-03-31 19:50:14 +0100] [62919] [INFO] Stopping worker [62919]
[2021-03-31 19:50:14 +0100] [62919] [INFO] Server Stopped
------------------------------------------------------------------ Captured log call ------------------------------------------------------------------
INFO     sanic.root:testing.py:73 http://127.0.0.1:52851/rest/users
INFO     sanic.access:http.py:435 
INFO     sanic.root:server.py:577 Starting worker [62919]
INFO     sanic.root:server.py:580 Stopping worker [62919]
INFO     sanic.root:app.py:928 Server Stopped
================================================================== warnings summary ===================================================================
.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6
  /private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys/.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6: PytestCollectionWarning: cannot collect test class 'TestManager' because it has a __init__ constructor (from: tests/test_main.py)
    class TestManager:

-- Docs: https://docs.pytest.org/en/stable/warnings.html
=============================================================== short test summary info ===============================================================
FAILED tests/test_main.py::test_json_payload - assert 400 == 200
============================================================ 1 failed, 1 warning in 5.43s =============================================================
$ pipenv run pytest tests
Loading .env environment variables...
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.2, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: /private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys
collected 1 item                                                                                                                                      

tests/test_main.py .                                                                                                                            [100%]

================================================================== warnings summary ===================================================================
.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6
  /private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys/.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6: PytestCollectionWarning: cannot collect test class 'TestManager' because it has a __init__ constructor (from: tests/test_main.py)
    class TestManager:

-- Docs: https://docs.pytest.org/en/stable/warnings.html
============================================================ 1 passed, 1 warning in 0.17s =============================================================

I've tried to hunt this down but have been unsuccessful. I've also tried this on different python versions, and machines. So I'm hoping you'll find the same inconsistent error/pass.

Here's the pipenv environment used with the versions for the dependancies:

Pipfile:

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
sanic = "*"
sanic-testing = "*"
pytest = "*"

[dev-packages]

[requires]
python_version = "3.9"

Pipfile.lock

{
    "_meta": {
        "hash": {
            "sha256": "a5e4466bac5fdc5f5fba3b90f03441ffb8a799a6c607ba412c25b1ff10d06652"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.9"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "aiofiles": {
            "hashes": [
                "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27",
                "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"
            ],
            "version": "==0.6.0"
        },
        "attrs": {
            "hashes": [
                "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
                "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
            ],
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
            "version": "==20.3.0"
        },
        "certifi": {
            "hashes": [
                "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
                "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
            ],
            "version": "==2020.12.5"
        },
        "h11": {
            "hashes": [
                "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
                "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
            ],
            "markers": "python_version >= '3.6'",
            "version": "==0.12.0"
        },
        "httpcore": {
            "hashes": [
                "sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9",
                "sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"
            ],
            "markers": "python_version >= '3.6'",
            "version": "==0.12.3"
        },
        "httptools": {
            "hashes": [
                "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
                "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
                "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
                "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
                "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
                "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
                "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
                "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
                "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
                "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
                "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
                "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
            ],
            "version": "==0.1.1"
        },
        "httpx": {
            "hashes": [
                "sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537",
                "sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"
            ],
            "markers": "python_version >= '3.6'",
            "version": "==0.16.1"
        },
        "idna": {
            "hashes": [
                "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16",
                "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"
            ],
            "version": "==3.1"
        },
        "iniconfig": {
            "hashes": [
                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
            ],
            "version": "==1.1.1"
        },
        "multidict": {
            "hashes": [
                "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a",
                "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93",
                "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632",
                "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656",
                "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79",
                "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7",
                "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d",
                "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5",
                "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224",
                "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26",
                "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea",
                "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348",
                "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6",
                "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76",
                "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1",
                "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f",
                "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952",
                "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a",
                "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37",
                "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9",
                "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359",
                "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8",
                "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da",
                "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3",
                "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d",
                "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf",
                "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841",
                "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d",
                "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93",
                "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f",
                "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647",
                "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635",
                "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456",
                "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda",
                "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5",
                "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
                "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
            ],
            "markers": "python_version >= '3.6'",
            "version": "==5.1.0"
        },
        "packaging": {
            "hashes": [
                "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
                "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
            ],
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
            "version": "==20.9"
        },
        "pluggy": {
            "hashes": [
                "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
                "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
            ],
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
            "version": "==0.13.1"
        },
        "py": {
            "hashes": [
                "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
                "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
            ],
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
            "version": "==1.10.0"
        },
        "pyparsing": {
            "hashes": [
                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
            ],
            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
            "version": "==2.4.7"
        },
        "pytest": {
            "hashes": [
                "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9",
                "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"
            ],
            "index": "pypi",
            "version": "==6.2.2"
        },
        "rfc3986": {
            "extras": [
                "idna2008"
            ],
            "hashes": [
                "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
                "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
            ],
            "version": "==1.4.0"
        },
        "sanic": {
            "hashes": [
                "sha256:84a04c5f12bf321bed3942597787f1854d15c18f157aebd7ced8c851ccc49e08",
                "sha256:9b63b0367f45a854023cb1f54a7315a86582442a08ba22f806c1f3ebf0c04e00"
            ],
            "index": "pypi",
            "version": "==21.3.2"
        },
        "sanic-routing": {
            "hashes": [
                "sha256:6abed924082f853100ad6bd1f0a207aa9a58840e43f3f53bbbc5459781aeb17a",
                "sha256:ae403650fe2d8d924edb60074e55c99657e260d7097c2fc50d0b3e1e08d3def4"
            ],
            "version": "==0.4.3"
        },
        "sanic-testing": {
            "hashes": [
                "sha256:67dd21e5309105fe3d037cc0e435d1dc7305dcdfde31feb8d13861a82f6ae95c",
                "sha256:a0a8a4d7bc0c5b2e2175c529bb4fa5edf3fd8a9317cdd582825997a91be9bfa9"
            ],
            "index": "pypi",
            "version": "==0.3.0"
        },
        "sniffio": {
            "hashes": [
                "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
                "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
            ],
            "markers": "python_version >= '3.5'",
            "version": "==1.2.0"
        },
        "toml": {
            "hashes": [
                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
            ],
            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
            "version": "==0.10.2"
        },
        "ujson": {
            "hashes": [
                "sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967",
                "sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1",
                "sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47",
                "sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720",
                "sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a",
                "sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108",
                "sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737",
                "sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394",
                "sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c",
                "sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c",
                "sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb",
                "sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939",
                "sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d",
                "sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99",
                "sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5",
                "sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa",
                "sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361",
                "sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e",
                "sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec",
                "sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c",
                "sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb"
            ],
            "markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
            "version": "==4.0.2"
        },
        "uvloop": {
            "hashes": [
                "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc",
                "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69",
                "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01",
                "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d",
                "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760",
                "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c",
                "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47",
                "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c",
                "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c",
                "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7"
            ],
            "markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
            "version": "==0.15.2"
        },
        "websockets": {
            "hashes": [
                "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
                "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
                "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
                "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
                "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
                "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
                "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
                "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
                "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
                "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
                "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
                "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
                "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
                "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
                "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
                "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
                "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
                "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
                "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
                "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
                "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
                "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
            ],
            "markers": "python_full_version >= '3.6.1'",
            "version": "==8.1"
        }
    },
    "develop": {}
}

Cannot patch the 'httpx' requests while unit testing

Sanic-testing uses httpx under the hood, which makes monkeypatching httpx requests hard to implement.

For example if we have and endpoint which makes external requests, and if we monkeypatch this request, then it patches an actual request to endpoint:

# views.py
import httpx
...
@app.get('/test')
async def view_test(request):
    async with httpx.AsyncClient() as client:
        api_response = await client.get(
            'https://jsonplaceholder.typicode.com/todos/1',
            timeout=10,
        )
        resp = api_response.json()
        resp['foo'] = 0
        return HTTPResponse(json.dumps(resp), 200)

Test function:

# test_views.py
import httpx, pytest
...
# The `client` parameter is the fixture of web app
def test_view_test(client, monkeypatch):
    async def return_mock_response(*args, **kwargs):
        return httpx.Response(200, content=b'{"response": "response"}')

    monkeypatch.setattr(httpx.AsyncClient, 'get', return_mock_response)
    _, response = client.test_client.get('/test')
    assert response.json == {'response': 'response', 'foo': 0}
    assert response.status_code == 200

Here, we replace our request _, response = client.test_client.get('/test') with the patched one. Which makes us customize the patch by excluding the host address.

Testing CORS fails

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

[Bug] async test fails

I tried to run the async test and it failed.

Error

_________________________________________________________ test_basic_asgi_client __________________________________________________________

app = Sanic(name="test_test")

    @pytest.mark.asyncio
    async def test_basic_asgi_client(app):
>       request, response = await app.asgi_client.get("/")

test_test.py:16: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib64/python3.10/site-packages/httpx/_client.py:1751: in get
    return await self.request(
.venv/lib64/python3.10/site-packages/sanic_testing/testing.py:364: in request
    await self.sanic_app._startup()  # type: ignore
.venv/lib64/python3.10/site-packages/sanic/app.py:1513: in _startup
    self.ext._display()
.venv/lib64/python3.10/site-packages/sanic_ext/bootstrap.py:110: in _display
    f"  > {extension.name} {extension.render_label()}"
.venv/lib64/python3.10/site-packages/sanic_ext/extensions/base.py:56: in render_label
    label = self.label()
.venv/lib64/python3.10/site-packages/sanic_ext/extensions/openapi/extension.py:25: in label
    return self._make_url()
.venv/lib64/python3.10/site-packages/sanic_ext/extensions/openapi/extension.py:34: in _make_url
    else self.app.serve_location
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = Sanic(name="test_test")

    @property
    def serve_location(self) -> str:
>       server_settings = self.state.server_info[0].settings
E       IndexError: list index out of range

.venv/lib64/python3.10/site-packages/sanic/mixins/runner.py:574: IndexError

How to reproduce

  1. create new venv and install packages
  2. create test_test.py file with this contents
import pytest
from sanic import Sanic, response

@pytest.fixture
def app():
    sanic_app = Sanic(__name__)

    @sanic_app.get("/")
    def basic(request):
        return response.text("foo")

    return sanic_app

@pytest.mark.asyncio
async def test_basic_asgi_client(app):
    request, response = await app.asgi_client.get("/")

    assert request.method.lower() == "get"
    assert response.body == b"foo"
    assert response.status == 200
  1. run pytest and watch it fail

Notes

  • Without sanic_ext tesing is successful
  • I can't just remove sanic_ext from my project

Packages

aiofiles==0.8.0
anyio==3.6.1
attrs==21.4.0
certifi==2022.6.15
h11==0.12.0
httpcore==0.15.0
httptools==0.4.0
httpx==0.23.0
idna==3.3
iniconfig==1.1.1
multidict==6.0.2
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.9
pytest==7.1.2
pytest-asyncio==0.18.3
PyYAML==6.0
rfc3986==1.5.0
sanic==22.6.0
sanic-ext==22.6.2
sanic-routing==22.3.0
sanic-testing==22.6.0
sniffio==1.2.0
tomli==2.0.1
ujson==5.4.0
uvloop==0.16.0
websockets==10.3

sanic 21.12 compatibility

sanic-testing 22.9 breaks compatibility with sanic 21.12:

>>> from sanic import Sanic
>>> app = Sanic('app')
>>> app.test_client.get('/')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../python3.9/site-packages/sanic_testing/testing.py", line 288, in get
    return self._sanic_endpoint_test("get", *args, **kwargs)
  File "/.../python3.9/site-packages/sanic_testing/testing.py", line 237, in _sanic_endpoint_test
    self.app.run(  # type: ignore
TypeError: run() got an unexpected keyword argument 'single_process'

sanic-testing 22.6 works though

Adding _test_manager object to Sanic App prevents App from being pickled

Example:

import pickle
from sanic import Sanic
from sanic_testing import TestManager

app = Sanic(__name__)
test_manager = TestManager(app)

my_dict = {"app": app}
my_pickled = pickle.dumps(my_dict)

Error:

>        pickle.dumps(my_dict)
E       AttributeError: Can't pickle local object 'SanicASGITestClient.__init__.<locals>._collect_request'

This is due to the use of nested functions in the ASGS TestClient methods, python does not know how to pickle object that use those.

ASGITestClient response type

After doing some mypy analysis, I've determined that the ASGITestClient request() method doesn't actually return a Sanic HTTPResponse, but instead a HTTPX Response object. Is this intended? Looks like theres an attempt to make it compatibile with a Sanic response object by adding in some extra attributes? Was that just to get some tests passing?

        response.status = response.status_code
        response.body = response.content
        response.content_type = response.headers.get("content-type")

Should we try to construct and return a real Sanic HTTPResponse here instead?

Mock Response and Stage in TestASGIApp

During work on sanic-org/sanic#2323, I found that current object that's used to mock stream, TestASGIApp, doesn't have stage and response attribute. I am not sure if it is worth, but it seems implement a mocked stage and response in TestASGIApp can make the testing more like the real use cases.

Test responses missing `Content-Length`

It seems as though sanic_testing does not full expose the final response.

import pytest
from sanic import Sanic
from sanic import response
from sanic_testing import TestManager


@pytest.fixture
def app():
    sanic_app = Sanic(__name__)
    TestManager(sanic_app)

    @sanic_app.get('/data.json')
    def get_data(request):
        return response.json({
            'data': [1, 2, 3, 4],
        })

    return sanic_app


@pytest.mark.asyncio
async def test_json_response(app):
    request, response = await app.asgi_client.get("/data.json")

    assert response.status == 200
    assert response.headers['Content-Type'] == 'application/json'
    assert response.headers['Content-Length'] == len(response.body)  # <- failure here

When running the same server and making a request to it, the Content-Length header is correctly set:

$ curl -v http://localhost:3000/data.json
*   Trying 127.0.0.1:3000...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /data.json HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-length: 18
< connection: keep-alive
< content-type: application/json
< 
* Connection #0 to host localhost left intact
{"data":[1,2,3,4]}

I would like to add that Content-Type is the only response header sent as well, so Transfer-Encoding is not set either.

[Bug] Websocket testing .recv()

I am trying to test a websocket endpoint that uses websocket.recv() inside, but this method is throwing an exception.

Error

ERROR    sanic.error:handlers.py:183 Exception occurred while handling uri: 'http:///ws'
Traceback (most recent call last):
  File "/worker/.local/lib/python3.7/site-packages/sanic/app.py", line 971, in _websocket_handler
    await fut
  File "/app/application_tests/test_sanic_testing.py", line 18, in handler
    await ws.recv()
  File "/worker/.local/lib/python3.7/site-packages/sanic/server/websockets/connection.py", line 49, in recv
    if message["type"] == "websocket.receive":
KeyError: 'type'

How to reproduce

  1. Create new venv and install packages
  2. Create test_test.py file with this contents
import pytest
from sanic import Sanic

from sanic_testing import TestManager


@pytest.fixture
def app():
    sanic_app = Sanic("test")
    TestManager(sanic_app)
    return sanic_app


@pytest.mark.asyncio
async def test_websocket_route(app):
    @app.websocket("/ws")
    async def handler(request, ws):
        await ws.recv()

    await app.asgi_client.websocket("/ws")
  1. Run pytest and see the error message

Requirements

Python 3.7

pytest-asyncio==0.19.0
sanic==22.6.1
sanic-testing==22.6.0

test_mode automatically enabled if the package "sanic-testing" is present

Describe the bug
I was wondering why my code was returning True for testing when I wasn't testing it (it behave differently when testing, such as not really sending emails).

After looking, I found out that Sanic/app.py loop over the SANIC_PACKAGES list, which includes "sanic-testing", and if present, import them.

When importing Sanic testing, this package change the test_mode to True, which is not true until the Sanic Testing package has been instantiated. IMHO, it should only change the state to true when enabled, not when imported

Code snippet

app.py, line 1836

            for package_name in SANIC_PACKAGES:
                module_name = package_name.replace("-", "_")
                try:
                    module = import_module(module_name)
                    packages.append(f"{package_name}=={module.__version__}")
                except ImportError:
                    ...

Expected behavior
Importing "sanic-testing" shouldn't change the state of Sanic (test_mode). Only when instantiating it.

Environment (please complete the following information):

  • OS: Fedora 32 v5.11.22-100.fc32.x86_64
  • Python: 3.8.10
  • Sanic version : 21.12.1

Potentially incorrect gather_request logic in TestClient

if gather_request:
def _collect_request(request):
if results[0] is None:
results[0] = request
self.app.request_middleware.appendleft(_collect_request)

The non-asgi client adds the collect_request middleware to the app if "gather_request" flag is True.
However multiple calls to test_client.request() will result in many copies of the collect_request middleware to be placed in the queue. This results in the collect_request middleware running multiple times, and it will even run if a later call to test_client.request() specifies gather_request=False.

app.request_middleware.appendleft(_collect_request)

The ASGI Test client gets around this issue by placing the "collect_request" middlware into the queue once only, then stores a "gather_request" flag on the test-client itself.
However this logic is incomplete, because while the self.gather_request variable is saved, it is never used or looked up during the request, so request is always gathered, even if "gather_request" is False.

self.gather_request = gather_request

Incorrect Return type on _sanic_endpoint_test

_sanic_endpoint_test can return response or (request, response) depending if gather_request is specified.

Tuple and Union are around the wrong way.

typing.Tuple[typing.Union[Request, HTTPResponse]]

Should be

typing.Union[typing.Tuple[Request, HTTPResponse], HTTPResponse]

Test Client Type Annotation and Common Parent Class

The type annotation of Sanic app's test_client currently is Any. Create a better type for it can make test dev experience better. I think adding a parent class, either abstract or non-abstract class, is one of the ways to solve this.

What is the best way to app.purge_tasks()?

For every test I run, my tests pass but I get the following error:

Task was destroyed but it is pending!
task: <Task pending name='Task-22' coro=<ModelRunner.model_runner() running at /opt/url-ml-server/app/request_batching/model_runner.py:77> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f665dd762b0>()]>>
Task was destroyed but it is pending!
task: <Task pending name='Task-11' coro=<ModelRunner.model_runner() running at /opt/url-ml-server/app/request_batching/model_runner.py:77> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f665dd762b0>()]>>
Task was destroyed but it is pending!
task: <Task pending name='Task-50' coro=<ModelRunner.model_runner() running at /opt/url-ml-server/app/request_batching/model_runner.py:77> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f665dd762b0>()]>>

@ahopkins Is there a way to purge tasks before app/server stops to avoid above warnings?

Getting AttributeError: __aenter__ on async with self.queue_lock:

Hi,

I wanted to enquire if there are examples of unit tests for this. I have been continuously struggling with unit tests for sanic async post method with the following error:

async with self.queue_lock:
AttributeError: __aenter__

I am getting this error for:

async with self.queue_lock:
        if len(self.queue) >= MAX_QUEUE_SIZE:

For more details, I am using a code from here: https://github.com/deep-learning-with-pytorch/dlwpt-code/blob/master/p3ch15/request_batching_server.py#L58

The test I am trying:

import json
import pytest
from ..app import app

@pytest.mark.asyncio
async def test_basic_post():
    _, response = await app.asgi_client.post("/predict", data=json.dumps(INPUT), headers=HEADERS)
    print(response)

Please do let me know if anyone has come across this and has a solution. Thank you.

CC: @ahopkins

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.