Skip to content

Commit c82a356

Browse files
committed
Added tests for AioHttpRouter and aiohttp in README.md
1 parent 2f062a9 commit c82a356

File tree

10 files changed

+426
-10
lines changed

10 files changed

+426
-10
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pip install fastopenapi
3030

3131
#### Install FastOpenAPI with a specific framework:
3232
```bash
33+
pip install fastopenapi[aiohttp]
34+
```
35+
```bash
3336
pip install fastopenapi[falcon]
3437
```
3538
```bash
@@ -60,6 +63,35 @@ pip install fastopenapi[tornado]
6063

6164
#### Examples:
6265

66+
- ![AIOHTTP](https://img.shields.io/badge/AioHttp-0078D7?style=flat&logo=python&logoColor=white)
67+
<details>
68+
<summary>Click to expand the Falcon Example</summary>
69+
70+
```python
71+
from aiohttp import web
72+
from pydantic import BaseModel
73+
74+
from fastopenapi.routers import AioHttpRouter
75+
76+
app = web.Application()
77+
router = AioHttpRouter(app=app)
78+
79+
80+
class HelloResponse(BaseModel):
81+
message: str
82+
83+
84+
@router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
85+
async def hello(name: str):
86+
"""Say hello from aiohttp"""
87+
return HelloResponse(message=f"Hello, {name}! It's aiohttp!")
88+
89+
90+
if __name__ == "__main__":
91+
web.run_app(app, host="127.0.0.1", port=8000)
92+
```
93+
</details>
94+
6395
- ![Falcon](https://img.shields.io/badge/Falcon-45b8d8?style=flat&logo=falcon&logoColor=white)
6496
<details>
6597
<summary>Click to expand the Falcon Example</summary>
@@ -267,7 +299,7 @@ http://127.0.0.1:8000/redoc
267299
## ⚙️ Features
268300
- **Generate OpenAPI schemas** with Pydantic v2.
269301
- **Data validation** using Pydantic models.
270-
- **Supports multiple frameworks:** Falcon, Flask, Quart, Sanic, Starlette, Tornado.
302+
- **Supports multiple frameworks:** AIOHTTP, Falcon, Flask, Quart, Sanic, Starlette, Tornado.
271303
- **Proxy routing provides FastAPI-style routing**
272304

273305
---

fastopenapi/base_router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,4 +371,6 @@ def resolve_endpoint_params(
371371
def openapi(self) -> dict:
372372
if self._openapi_schema is None:
373373
self._openapi_schema = self.generate_openapi()
374+
# We don't need model cache anymore
375+
self.__class__._model_schema_cache.clear()
374376
return self._openapi_schema

fastopenapi/routers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def __init__(self, *args, **kwargs):
66
try:
77
from fastopenapi.routers.aiohttp import AioHttpRouter
88
except ModuleNotFoundError:
9-
FalconRouter = AioHttpRouter
9+
AioHttpRouter = MissingRouter
1010

1111
try:
1212
from fastopenapi.routers.falcon import FalconRouter
@@ -39,6 +39,7 @@ def __init__(self, *args, **kwargs):
3939
TornadoRouter = MissingRouter
4040

4141
__all__ = [
42+
"AioHttpRouter",
4243
"FalconRouter",
4344
"FlaskRouter",
4445
"QuartRouter",

fastopenapi/routers/aiohttp.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,3 @@ async def redoc_view(request):
8181
self.app.router.add_route("GET", self.openapi_url, openapi_view)
8282
self.app.router.add_route("GET", self.docs_url, docs_view)
8383
self.app.router.add_route("GET", self.redoc_url, redoc_view)
84-
85-
def configure_routes(self, app: web.Application):
86-
"""
87-
Configure routes for an application when not passed during initialization
88-
"""
89-
for path, method, view in self._routes_aiohttp:
90-
app.router.add_route(method.upper(), path, view)
91-
self._register_docs_endpoints()

tests/aiohttp/__init__.py

Whitespace-only changes.

tests/aiohttp/conftest.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import pytest
2+
from aiohttp import web
3+
from pydantic import BaseModel
4+
5+
from fastopenapi.routers import AioHttpRouter
6+
7+
8+
class Item(BaseModel):
9+
id: int
10+
name: str
11+
description: str = None
12+
13+
14+
class CreateItemRequest(BaseModel):
15+
name: str
16+
description: str = None
17+
18+
19+
class ItemResponse(BaseModel):
20+
id: int
21+
name: str
22+
description: str = None
23+
24+
25+
@pytest.fixture
26+
async def dummy_endpoint():
27+
return {"message": "dummy"}
28+
29+
30+
@pytest.fixture
31+
def items_db():
32+
return [
33+
{"id": 1, "name": "Item 1", "description": "Description 1"},
34+
{"id": 2, "name": "Item 2", "description": "Description 2"},
35+
]
36+
37+
38+
@pytest.fixture
39+
def app(items_db): # noqa: C901
40+
app = web.Application()
41+
router = AioHttpRouter(
42+
app=app,
43+
title="Test API",
44+
description="Test API for AioHttpRouter",
45+
version="0.1.0",
46+
)
47+
48+
@router.get("/items", response_model=list[ItemResponse], tags=["items"])
49+
async def get_items():
50+
return [Item(**item) for item in items_db]
51+
52+
@router.get("/items-sync", response_model=list[ItemResponse], tags=["items"])
53+
def get_items_sync():
54+
return [Item(**item) for item in items_db]
55+
56+
@router.get("/items-fail", response_model=list[ItemResponse], tags=["items"])
57+
async def get_items_fail():
58+
raise Exception("TEST ERROR")
59+
60+
@router.get("/items/{item_id}", response_model=ItemResponse, tags=["items"])
61+
async def get_item(item_id: int):
62+
for item in items_db:
63+
if item["id"] == item_id:
64+
return Item(**item)
65+
raise web.HTTPNotFound(text="Not Found")
66+
67+
@router.post("/items", response_model=ItemResponse, status_code=201, tags=["items"])
68+
async def create_item(item: CreateItemRequest):
69+
new_id = max(item_["id"] for item_ in items_db) + 1
70+
new_item = {"id": new_id, "name": item.name, "description": item.description}
71+
items_db.append(new_item)
72+
return Item(**new_item)
73+
74+
@router.put("/items/{item_id}", response_model=ItemResponse, tags=["items"])
75+
async def update_item(item_id: int, item: CreateItemRequest):
76+
for existing_item in items_db:
77+
if existing_item["id"] == item_id:
78+
existing_item["name"] = item.name
79+
existing_item["description"] = item.description
80+
return Item(**existing_item)
81+
raise web.HTTPNotFound(text="Not Found")
82+
83+
@router.delete("/items/{item_id}", status_code=204, tags=["items"])
84+
async def delete_item(item_id: int):
85+
for i, item in enumerate(items_db):
86+
if item["id"] == item_id:
87+
del items_db[i]
88+
return None
89+
raise web.HTTPNotFound(text="Not Found")
90+
91+
return app
92+
93+
94+
@pytest.fixture
95+
def client(app, event_loop):
96+
from aiohttp.test_utils import TestClient, TestServer
97+
98+
server = TestServer(app, loop=event_loop)
99+
event_loop.run_until_complete(server.start_server())
100+
client = TestClient(server, loop=event_loop)
101+
event_loop.run_until_complete(client.start_server())
102+
yield client
103+
event_loop.run_until_complete(client.close())
104+
event_loop.run_until_complete(server.close())
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import json
2+
3+
import pytest
4+
5+
6+
class TestAioHttpIntegration:
7+
@pytest.mark.asyncio
8+
async def test_get_items(self, client):
9+
"""Test retrieving all items"""
10+
resp = await client.get("/items")
11+
assert resp.status == 200
12+
data = await resp.json()
13+
assert len(data) == 2
14+
assert data[0]["name"] == "Item 1"
15+
assert data[1]["name"] == "Item 2"
16+
17+
@pytest.mark.asyncio
18+
async def test_get_items_sync(self, client):
19+
"""Test retrieving all items (synchronous endpoint)"""
20+
resp = await client.get("/items-sync")
21+
assert resp.status == 200
22+
data = await resp.json()
23+
assert len(data) == 2
24+
assert data[0]["name"] == "Item 1"
25+
assert data[1]["name"] == "Item 2"
26+
27+
@pytest.mark.asyncio
28+
async def test_get_items_fail(self, client):
29+
"""Test retrieving items with generated error"""
30+
resp = await client.get("/items-fail")
31+
assert resp.status == 500
32+
data = await resp.json()
33+
assert data["detail"] == "TEST ERROR"
34+
35+
@pytest.mark.asyncio
36+
async def test_get_item(self, client):
37+
"""Test retrieving item by ID"""
38+
resp = await client.get("/items/1")
39+
assert resp.status == 200
40+
data = await resp.json()
41+
assert data["id"] == 1
42+
assert data["name"] == "Item 1"
43+
assert data["description"] == "Description 1"
44+
45+
@pytest.mark.asyncio
46+
async def test_get_item_unprocessable(self, client):
47+
"""Test retrieving item with incorrect parameter type"""
48+
resp = await client.get("/items/abc")
49+
assert resp.status == 422
50+
data = await resp.json()
51+
assert "Error casting parameter" in data["detail"]
52+
53+
@pytest.mark.asyncio
54+
async def test_get_nonexistent_item(self, client):
55+
"""Test retrieving non-existent item"""
56+
resp = await client.get("/items/999")
57+
assert resp.status == 404
58+
59+
@pytest.mark.asyncio
60+
async def test_create_item(self, client):
61+
"""Test creating an item"""
62+
new_item = {"name": "New Item", "description": "New Description"}
63+
headers = {"Content-Type": "application/json"}
64+
resp = await client.post("/items", data=json.dumps(new_item), headers=headers)
65+
assert resp.status == 201
66+
data = await resp.json()
67+
assert data["id"] == 3
68+
assert data["name"] == "New Item"
69+
assert data["description"] == "New Description"
70+
71+
@pytest.mark.asyncio
72+
async def test_create_item_incorrect(self, client):
73+
"""Test creating an item with invalid data"""
74+
new_item = {"name": None, "description": "New Description"}
75+
headers = {"Content-Type": "application/json"}
76+
resp = await client.post("/items", data=json.dumps(new_item), headers=headers)
77+
assert resp.status == 422
78+
data = await resp.json()
79+
assert "Validation error for parameter" in data["detail"]
80+
81+
@pytest.mark.asyncio
82+
async def test_create_item_invalid_json(self, client):
83+
"""Test creating an item with invalid JSON"""
84+
headers = {"Content-Type": "application/json"}
85+
resp = await client.post("/items", data="incorrect json", headers=headers)
86+
assert resp.status == 422
87+
data = await resp.json()
88+
assert "Validation error for parameter" in data["detail"]
89+
90+
@pytest.mark.asyncio
91+
async def test_update_item(self, client):
92+
"""Test updating an item"""
93+
update_data = {"name": "Updated Item", "description": "Updated Description"}
94+
headers = {"Content-Type": "application/json"}
95+
resp = await client.put(
96+
"/items/2", data=json.dumps(update_data), headers=headers
97+
)
98+
assert resp.status == 200
99+
data = await resp.json()
100+
assert data["id"] == 2
101+
assert data["name"] == "Updated Item"
102+
assert data["description"] == "Updated Description"
103+
104+
@pytest.mark.asyncio
105+
async def test_delete_item(self, client):
106+
"""Test deleting an item"""
107+
resp = await client.delete("/items/1")
108+
assert resp.status == 204
109+
110+
# Check that the item was actually deleted
111+
resp = await client.get("/items/1")
112+
assert resp.status == 404
113+
114+
@pytest.mark.asyncio
115+
async def test_openapi_schema_endpoint(self, client):
116+
"""Test OpenAPI schema endpoint"""
117+
resp = await client.get("/openapi.json")
118+
assert resp.status == 200
119+
schema = await resp.json()
120+
assert schema["info"]["title"] == "Test API"
121+
assert "/items" in schema["paths"]
122+
assert "/items/{item_id}" in schema["paths"]
123+
124+
@pytest.mark.asyncio
125+
async def test_swagger_ui_endpoint(self, client):
126+
"""Test Swagger UI endpoint"""
127+
resp = await client.get("/docs")
128+
assert resp.status == 200
129+
text = await resp.text()
130+
assert "text/html" in resp.headers["Content-Type"]
131+
assert "swagger-ui" in text
132+
133+
@pytest.mark.asyncio
134+
async def test_redoc_ui_endpoint(self, client):
135+
"""Test ReDoc UI endpoint"""
136+
resp = await client.get("/redoc")
137+
assert resp.status == 200
138+
text = await resp.text()
139+
assert "text/html" in resp.headers["Content-Type"]
140+
assert "redoc" in text

0 commit comments

Comments
 (0)