Can we use Flask test_client() inside async function - python

I'm trying to test Flask REST-API end points with pytest using test_client(). But I'm getting an Error saying
> RuntimeError: You cannot use AsyncToSync in the same thread as an async event loop - just await the async function directly.
Can Anyone explain me why this happen and what is the solution to avoid this Error.
Test Functiob:
import pytest
from unittest import mock
from flask import request
from app import create_app
from app.base.views import views
app = create_app()
async def save_token_(sugar, payload, instance, bool, domain_id):
return {'valid':True}
payload = {
"password": 'password',
"username": 'crm_admin',
"grant_type": "password"
}
#pytest.mark.asyncio
async def test_post_sugar_token(monkeypatch, aiohttp_client, loop):
mock_save_token = mock.AsyncMock(name = "mock_save_token")
mock_save_token.return_value = await save_token_(None, payload, 'domain.org', True, 89)
monkeypatch.setattr(views, 'save_token', mock_save_token)
await views.save_token(None, payload, 'domain.org', True, 9)
assert mock_save_token.call_args_list == [mock.call(None, payload, 'domain.org', True, 89)]
headers = {'Autherization': 'ehrdmek2492.fkeompvmw.04294002'}
data = {
'password': '12345',
'key':'Hi',
'instance': 'my.domain',
'domain_id': 1
}
##-# using test_client()
client = app.test_client()
res = client.post('/token/sugar/', data = data, headers = headers)
assert res.status_code == 200
assert res.content_type == 'application/json'
assert res.json == {'valid':True}
# # ----------------------------------------------------------------------
Error Message

I ran into the same issue and opened a ticket on flask's github repo:
https://github.com/pallets/flask/issues/4375.
They kindly explained the issue and provided a workaround.
In short, flask can handle async views, and flask's test_client can be used in an async context, but you cannot use both at the same time.
Quoting from the github issue:
The problem in this case is that the Flask codebase is not compatible with asyncio, so you cannot run the test client inside an asyncio loop. There is really nothing to gain from writing your unit test as an async test, since Flask itself isn't async.
Here's the workaround suggested, slightly re-adjusted for your example:
#pytest.mark.asyncio
async def test_post_sugar_token():
# ... same code as before
##-# using test_client()
def sync_test():
with app.test_client() as client:
res = client.post('/token/sugar/', data = data, headers = headers)
assert res.status_code == 200
assert res.content_type == 'application/json'
assert res.json == {'valid':True}
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, sync_test)

Related

Python Aiohttp: cookies behaviour on same domain

Hopefully this is not a too stupid question, but I am having trouble with aiohttp cookie processing.
Aiohttp's CookieJar class mentions it implements cookie storage adhering to RFC 6265, which states that:
cookies for a given host are shared across all the ports on that host
Cookies do not provide isolation by port. If a cookie is readable by a service running on one port, the cookie is also readable by a service running on another port of the same server.
But if I create two aiohttp servers, one that makes you "login" and gives you a cookie back, and another one with an endpoint that expects you to have a cookie, both hosted on localhost (two different ports I guess), the cookie will not be processed.
Here's a set of 4 tests using aiohttp, pytest, pytest and pytest-aiohttp to explain:
import functools
import pytest
from aiohttp import web
pytestmark = pytest.mark.asyncio
def attach_session(f):
#functools.wraps(f)
async def wrapper(request: web.Request):
session_id = request.cookies.get("testcookie")
request["mysession"] = session_id
response = await f(request)
response.set_cookie("testcookie", session_id)
return response
return wrapper
def is_logged_in(f):
#functools.wraps(f)
#attach_session
async def wrapper(request: web.Request):
session = request["mysession"]
if not session:
raise web.HTTPUnauthorized
return await f(request)
return wrapper
async def login(_: web.Request):
response = web.Response()
response.set_cookie("testcookie", "somerandomstring")
return response
#is_logged_in
async def some_endpoint(request: web.Request):
return web.Response(text="sweet")
#pytest.fixture
def auth_client(event_loop, aiohttp_client):
app = web.Application()
app.router.add_post("/login", login)
return event_loop.run_until_complete(aiohttp_client(app))
#pytest.fixture
def core_client(event_loop, aiohttp_client):
app = web.Application()
app.router.add_get("/some_endpoint", some_endpoint)
return event_loop.run_until_complete(aiohttp_client(app))
async def test_login(auth_client):
resp = await auth_client.post("/login")
assert resp.status == 200
assert resp.cookies.get("testcookie").value == "somerandomstring"
async def test_some_endpoint_anonymous(core_client):
resp = await core_client.get("/some_endpoint")
assert resp.status == 401
async def test_some_endpoint_as_logged_in(auth_client, core_client):
resp1 = await auth_client.post("/login")
resp2 = await core_client.get("/some_endpoint", cookies=resp1.cookies)
assert resp2.status == 401
async def test_some_endpoint_as_logged_in_again(auth_client, core_client):
resp1 = await auth_client.post("/login")
_cookie = list(resp1.cookies.values())[0]
resp2 = await core_client.get(
"/some_endpoint", cookies={_cookie.key: _cookie.value}
)
assert resp2.status == 200
But from my understanding, the "test_some_endpoint_as_logged_in" test should work. Why is it returning 401, while the same thing but with sending the cookie as a dict returns 200?
I think the correct way of sharing the cookies between clients would be loading the SimpleCookie object of the resp1 to the core_client.session.cookie_jar.
Changing the code of the test_some_endpoint_as_logged_in to should fix it:
async def test_some_endpoint_as_logged_in(auth_client, core_client):
resp1 = await auth_client.post("/login")
core_client.session.cookie_jar.update_cookies(resp1.cookies)
resp2 = await core_client.get("/some_endpoint")
assert resp2.status == 401
Cookie data is kept in the session object as the auth_client and core_client are different sessions with there own data cookie data is not shared. It is comparable to using a different browser with each there own cookie_jar.

How to mock client IP on fastapi.testclient.TestClient?

Say you are using fastapi.testclient.TestClient to perform a GET, for instance. Inside the API code that defines that GET method, if you get request.client.host, you will get the string "testclient".
Test, using pytest:
def test_success(self):
client = TestClient(app)
client.get('/my_ip')
Now, lets assume your API code is something like this:
#router.get('/my_ip')
def my_ip(request: Request):
return request.client.host
The endpoit /my_ip is suppose to return the client IP, but when running pytest, it will return "testclient" string. Is there a way to change the client IP (host) on TestClient to something other than "testclient"?
You can mock the fastapi.Request.client property as,
# main.py
from fastapi import FastAPI, Request
app = FastAPI()
#app.get("/")
def root(request: Request):
return {"host": request.client.host}
# test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_main(mocker):
mock_client = mocker.patch("fastapi.Request.client")
mock_client.host = "192.168.123.132"
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"host": "192.168.123.132"}

How to test #app.on_event("shutdown") in FastAPI?

I have a simple FastAPI setup as below,
# main.py
from fastapi import FastAPI
app = FastAPI()
#app.on_event("shutdown")
def app_shutdown():
with open("shutdown-test-file.txt", "w") as fp:
fp.write("FastAPI app has been terminated")
#app.get("/")
def root():
return {"message": "Hello World"}
How can I write (unit)test for this app_shutdown(...) functionality?
Related Posts
This SO post is also asking similar question, but, not in a "testing context"
The official doc has something similar, but, there is no example for on_event("shutdown")
According to the documentation, you need to wrap it in the context manager (a with statement) to trigger the events, something like this:
def test_read_items():
with TestClient(app) as client:
response = client.get("/items/foo")
assert response.status_code == 200
If you use pytest, you can set up a fixture for it like this:
from main import app
from fastapi.testclient import TestClient
import pytest
#pytest.fixture
def client():
with TestClient(app) as c:
yield c
def test_read_main(client):
response = client.get("/")
assert response.status_code == 200

Mocking aiohttp ClientSession contextmanager using asynctest

I have the following async function:
async def my_func(request):
# preparation code here
with aiohttp.ClientSession() as s:
with s.post(url, headers) as response:
status_code = response.status
if status_code == 200:
json_resp = await response.json()
elif:
# more checks
Trying to test it but still haven't found a way.
from asynctest.mock import CoroutineMock, MagicMock as AsyncMagicMock
#mock.patch("path_to_function.aiohttp.ClientSession", new_callable=AsyncMagicMock) # this is unittest.mock
def test_async_function(self, mocked_session):
s = AsyncMagicMock()
mocked_client_session().__aenter__ = CoroutineMock(side_effect=s)
session_post = s.post()
response_mock = AsyncMagicMock()
session_post.__aenter__ = CoroutineMock(side_effect=response_mock)
response_mock.status = 200
but not working as I want. Any help on how to test context managers would be highly appreciated.
Silly me, Found the solution. I was using side_effect instead of return_value.Works like a charm. Thank you very much

How to get dynamic path params from route in aiohttp when mocking the request?

Using the below route definition, I am trying to extract the book_id out of the URL in aiohttp.
from aiohttp import web
routes = web.RouteTableDef()
#routes.get('/books/{book_id}')
async def get_book_pages(request: web.Request) -> web.Response:
book_id = request.match_info.get('book_id', None)
return web.json_response({'book_id': book_id})
Below is the test (using pytest) I have written
import asynctest
import pytest
import json
async def test_get_book() -> None:
request = make_mocked_request('GET', '/books/1')
response = await get_book(request)
assert 200 == response.status
body = json.loads(response.body)
assert 1 == body['book_id']
Test Result:
None != 1
Expected :1
Actual :None
Outside of the tests, when I run a request to /books/1 the response is {'book_id': 1}
What is the correct way to retrieve dynamic values from the path in aiohttp when mocking the request?
make_mocked_request() knows nothing about an application and its routes.
To pass dynamic info you need to provide a custom match_info object:
async def test_get_book() -> None:
request = make_mocked_request('GET', '/books/1',
match_info={'book_id': '1'})
response = await get_book(request)
assert 200 == response.status
body = json.loads(response.body)
assert 1 == body['book_id']
P.S.
In general, I want to warn about mocks over-usage. Usually, functional testing with aiohttp_client is easier to read and maintain.
I prefer mocking for really hard-to-rest things like network errors emulation.
Otherwise your tests do test your own mocks, not a real code.

Categories