codeforge-dev 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/.env +22 -0
- package/.devcontainer/CHANGELOG.md +197 -0
- package/.devcontainer/CLAUDE.md +117 -0
- package/.devcontainer/README.md +222 -0
- package/.devcontainer/config/main-system-prompt.md +502 -0
- package/.devcontainer/config/settings.json +47 -0
- package/.devcontainer/devcontainer.json +94 -0
- package/.devcontainer/features/README.md +113 -0
- package/.devcontainer/features/agent-browser/README.md +65 -0
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +23 -0
- package/.devcontainer/features/agent-browser/install.sh +79 -0
- package/.devcontainer/features/ast-grep/README.md +24 -0
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +24 -0
- package/.devcontainer/features/ast-grep/install.sh +51 -0
- package/.devcontainer/features/ccstatusline/README.md +296 -0
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +19 -0
- package/.devcontainer/features/ccstatusline/install.sh +290 -0
- package/.devcontainer/features/ccusage/README.md +205 -0
- package/.devcontainer/features/ccusage/devcontainer-feature.json +38 -0
- package/.devcontainer/features/ccusage/install.sh +132 -0
- package/.devcontainer/features/claude-code/README.md +498 -0
- package/.devcontainer/features/claude-code/config/settings.json +36 -0
- package/.devcontainer/features/claude-code/config/system-prompt.md +118 -0
- package/.devcontainer/features/claude-code/config/world-building-sp.md +1432 -0
- package/.devcontainer/features/claude-code/devcontainer-feature.json +42 -0
- package/.devcontainer/features/claude-code/install.sh +466 -0
- package/.devcontainer/features/claude-monitor/README.md +74 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +38 -0
- package/.devcontainer/features/claude-monitor/install.sh +99 -0
- package/.devcontainer/features/lsp-servers/README.md +85 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +40 -0
- package/.devcontainer/features/lsp-servers/install.sh +116 -0
- package/.devcontainer/features/mcp-qdrant/CHANGES.md +399 -0
- package/.devcontainer/features/mcp-qdrant/README.md +474 -0
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +57 -0
- package/.devcontainer/features/mcp-qdrant/install.sh +295 -0
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +129 -0
- package/.devcontainer/features/mcp-reasoner/README.md +177 -0
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +20 -0
- package/.devcontainer/features/mcp-reasoner/install.sh +177 -0
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +67 -0
- package/.devcontainer/features/notify-hook/README.md +86 -0
- package/.devcontainer/features/notify-hook/devcontainer-feature.json +23 -0
- package/.devcontainer/features/notify-hook/install.sh +38 -0
- package/.devcontainer/features/splitrail/README.md +140 -0
- package/.devcontainer/features/splitrail/devcontainer-feature.json +34 -0
- package/.devcontainer/features/splitrail/install.sh +129 -0
- package/.devcontainer/features/tree-sitter/README.md +138 -0
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +52 -0
- package/.devcontainer/features/tree-sitter/install.sh +173 -0
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +106 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-file.py +101 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +137 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/SKILL.md +387 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/cli-flags-and-output.md +312 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/claude-code-headless/references/sdk-and-mcp.md +569 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/SKILL.md +309 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/compose-services.md +438 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker/references/dockerfile-patterns.md +340 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/SKILL.md +412 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/container-lifecycle.md +388 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/docker-py/references/resources-and-security.md +444 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/SKILL.md +344 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/middleware-and-lifespan.md +254 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/pydantic-models.md +245 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/routing-and-dependencies.md +255 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/fastapi/references/sse-and-streaming.md +318 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/SKILL.md +345 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/agents-and-tools.md +271 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/pydantic-ai/references/models-and-streaming.md +422 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/SKILL.md +220 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/cross-vendor-principles.md +139 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/patterns-and-antipatterns.md +376 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/skill-building/references/skill-authoring-patterns.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/SKILL.md +329 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/advanced-queries.md +314 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/javascript-patterns.md +323 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/python-patterns.md +354 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/sqlite/references/schema-and-pragmas.md +326 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/SKILL.md +356 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/ai-sdk-svelte.md +128 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/component-patterns.md +332 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/layercake.md +203 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/migration-guide.md +350 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/runes-and-reactivity.md +328 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/spa-and-routing.md +262 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/svelte5/references/svelte-dnd-action.md +181 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/SKILL.md +414 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/fastapi-testing.md +411 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codedirective-skills/skills/testing/references/svelte-testing.md +538 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +110 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/planning-reminder/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/.claude-plugin/plugin.json +7 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +17 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +108 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272create-pr.md +337 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272new.md +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272review-commit.md +290 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/commands/ticket/357/200/272work.md +257 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +8 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +184 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/.claude-plugin/plugin.json +6 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/config/planning-instructions.md +14 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/functional-conjuring-map.md +989 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/hooks/hooks.json +33 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/__pycache__/post-enhance-task.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhance-planning.py +71 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-plan.sh +68 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/enhancers/enhance-task.sh +120 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-plan.py +133 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workflow-enhancer/scripts/post-enhance-task.py +253 -0
- package/.devcontainer/scripts/setup-aliases.sh +80 -0
- package/.devcontainer/scripts/setup-config.sh +28 -0
- package/.devcontainer/scripts/setup-irie-claude.sh +32 -0
- package/.devcontainer/scripts/setup-plugins.sh +80 -0
- package/.devcontainer/scripts/setup.sh +58 -0
- package/LICENSE.txt +674 -0
- package/README.md +267 -0
- package/package.json +44 -0
- package/setup.js +83 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# FastAPI Testing -- Deep Dive
|
|
2
|
+
|
|
3
|
+
## 1. AsyncClient Setup
|
|
4
|
+
|
|
5
|
+
### Basic Fixture
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import pytest
|
|
9
|
+
from httpx import ASGITransport, AsyncClient
|
|
10
|
+
from app.main import app
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
async def client():
|
|
14
|
+
transport = ASGITransport(app=app)
|
|
15
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
16
|
+
yield ac
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### With Lifespan Events
|
|
20
|
+
|
|
21
|
+
`AsyncClient` does not trigger ASGI lifespan events. Install `asgi-lifespan` to handle startup/shutdown:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from asgi_lifespan import LifespanManager
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
async def client():
|
|
28
|
+
async with LifespanManager(app) as manager:
|
|
29
|
+
transport = ASGITransport(app=manager.app)
|
|
30
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
31
|
+
yield ac
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### pytest Configuration
|
|
35
|
+
|
|
36
|
+
```toml
|
|
37
|
+
# pyproject.toml
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
With pytest-anyio (alternative to pytest-asyncio):
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Mark async tests explicitly when not using `asyncio_mode = "auto"`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
@pytest.mark.anyio
|
|
54
|
+
async def test_endpoint(client: AsyncClient):
|
|
55
|
+
response = await client.get("/")
|
|
56
|
+
assert response.status_code == 200
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2. Dependency Override Patterns
|
|
62
|
+
|
|
63
|
+
### Basic Override
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from app.dependencies import get_db
|
|
67
|
+
|
|
68
|
+
async def mock_db():
|
|
69
|
+
yield FakeDatabase()
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
async def client():
|
|
73
|
+
app.dependency_overrides[get_db] = mock_db
|
|
74
|
+
transport = ASGITransport(app=app)
|
|
75
|
+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
76
|
+
yield ac
|
|
77
|
+
app.dependency_overrides.clear()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Authentication Bypass
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from app.dependencies import get_current_user
|
|
84
|
+
from app.models import User
|
|
85
|
+
|
|
86
|
+
async def mock_admin():
|
|
87
|
+
return User(id=1, email="admin@test.com", role="admin")
|
|
88
|
+
|
|
89
|
+
async def mock_regular_user():
|
|
90
|
+
return User(id=2, email="user@test.com", role="user")
|
|
91
|
+
|
|
92
|
+
@pytest.fixture
|
|
93
|
+
async def admin_client():
|
|
94
|
+
app.dependency_overrides[get_current_user] = mock_admin
|
|
95
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
96
|
+
yield ac
|
|
97
|
+
app.dependency_overrides.clear()
|
|
98
|
+
|
|
99
|
+
@pytest.fixture
|
|
100
|
+
async def user_client():
|
|
101
|
+
app.dependency_overrides[get_current_user] = mock_regular_user
|
|
102
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
103
|
+
yield ac
|
|
104
|
+
app.dependency_overrides.clear()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Parameterized Override Fixture
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
@pytest.fixture
|
|
111
|
+
def override_user():
|
|
112
|
+
"""Factory fixture for custom user overrides."""
|
|
113
|
+
def _override(user: User):
|
|
114
|
+
app.dependency_overrides[get_current_user] = lambda: user
|
|
115
|
+
return _override
|
|
116
|
+
|
|
117
|
+
@pytest.mark.anyio
|
|
118
|
+
async def test_owner_access(client, override_user):
|
|
119
|
+
override_user(User(id=5, email="owner@test.com", role="owner"))
|
|
120
|
+
response = await client.get("/admin/settings")
|
|
121
|
+
assert response.status_code == 200
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 3. SSE Stream Testing
|
|
127
|
+
|
|
128
|
+
### With httpx-sse
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pip install httpx-sse
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import pytest
|
|
136
|
+
from httpx import ASGITransport, AsyncClient
|
|
137
|
+
from httpx_sse import aconnect_sse
|
|
138
|
+
from app.main import app
|
|
139
|
+
|
|
140
|
+
@pytest.mark.anyio
|
|
141
|
+
async def test_sse_stream():
|
|
142
|
+
async with AsyncClient(
|
|
143
|
+
transport=ASGITransport(app=app),
|
|
144
|
+
base_url="http://test"
|
|
145
|
+
) as ac:
|
|
146
|
+
async with aconnect_sse(ac, "GET", "/events") as event_source:
|
|
147
|
+
events = []
|
|
148
|
+
async for sse in event_source.aiter_sse():
|
|
149
|
+
events.append(sse)
|
|
150
|
+
if len(events) >= 3:
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
assert len(events) == 3
|
|
154
|
+
assert events[0].event == "update"
|
|
155
|
+
assert events[0].data # non-empty data
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Asserting Event Structure
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
@pytest.mark.anyio
|
|
162
|
+
async def test_sse_event_types():
|
|
163
|
+
async with AsyncClient(
|
|
164
|
+
transport=ASGITransport(app=app),
|
|
165
|
+
base_url="http://test"
|
|
166
|
+
) as ac:
|
|
167
|
+
async with aconnect_sse(ac, "POST", "/chat", json={"prompt": "Hello"}) as event_source:
|
|
168
|
+
events = []
|
|
169
|
+
async for sse in event_source.aiter_sse():
|
|
170
|
+
events.append({"event": sse.event, "data": sse.data})
|
|
171
|
+
|
|
172
|
+
# Verify event sequence
|
|
173
|
+
event_types = [e["event"] for e in events]
|
|
174
|
+
assert "token" in event_types
|
|
175
|
+
assert event_types[-1] == "done"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Raw Stream Testing (Without httpx-sse)
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
@pytest.mark.anyio
|
|
182
|
+
async def test_sse_raw_stream():
|
|
183
|
+
async with AsyncClient(
|
|
184
|
+
transport=ASGITransport(app=app),
|
|
185
|
+
base_url="http://test"
|
|
186
|
+
) as ac:
|
|
187
|
+
async with ac.stream("GET", "/events") as response:
|
|
188
|
+
assert response.status_code == 200
|
|
189
|
+
assert "text/event-stream" in response.headers["content-type"]
|
|
190
|
+
|
|
191
|
+
lines = []
|
|
192
|
+
async for line in response.aiter_lines():
|
|
193
|
+
if line.startswith("data:"):
|
|
194
|
+
lines.append(line.removeprefix("data: "))
|
|
195
|
+
assert len(lines) >= 1
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 4. WebSocket Testing
|
|
201
|
+
|
|
202
|
+
WebSocket testing uses the synchronous `TestClient`:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from fastapi.testclient import TestClient
|
|
206
|
+
from app.main import app
|
|
207
|
+
|
|
208
|
+
def test_websocket_echo():
|
|
209
|
+
client = TestClient(app)
|
|
210
|
+
with client.websocket_connect("/ws") as ws:
|
|
211
|
+
ws.send_text("hello")
|
|
212
|
+
data = ws.receive_text()
|
|
213
|
+
assert data == "Echo: hello"
|
|
214
|
+
|
|
215
|
+
def test_websocket_json():
|
|
216
|
+
client = TestClient(app)
|
|
217
|
+
with client.websocket_connect("/ws/json") as ws:
|
|
218
|
+
ws.send_json({"action": "ping"})
|
|
219
|
+
response = ws.receive_json()
|
|
220
|
+
assert response["action"] == "pong"
|
|
221
|
+
|
|
222
|
+
def test_websocket_disconnect():
|
|
223
|
+
client = TestClient(app)
|
|
224
|
+
with client.websocket_connect("/ws") as ws:
|
|
225
|
+
ws.send_text("hello")
|
|
226
|
+
ws.receive_text()
|
|
227
|
+
# Connection is closed after the context manager exits
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Available WebSocket test methods:
|
|
231
|
+
|
|
232
|
+
| Method | Purpose |
|
|
233
|
+
|--------|---------|
|
|
234
|
+
| `ws.send_text(data)` | Send text frame |
|
|
235
|
+
| `ws.send_bytes(data)` | Send binary frame |
|
|
236
|
+
| `ws.send_json(data)` | Send JSON |
|
|
237
|
+
| `ws.receive_text()` | Receive text frame |
|
|
238
|
+
| `ws.receive_bytes()` | Receive binary frame |
|
|
239
|
+
| `ws.receive_json()` | Receive JSON |
|
|
240
|
+
| `ws.close(code=1000)` | Close connection |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 5. Background Task Testing
|
|
245
|
+
|
|
246
|
+
### Assert Task Was Scheduled
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from unittest.mock import AsyncMock, patch
|
|
250
|
+
|
|
251
|
+
@pytest.mark.anyio
|
|
252
|
+
async def test_order_sends_notification(client: AsyncClient):
|
|
253
|
+
with patch("app.tasks.send_notification", new_callable=AsyncMock) as mock_notify:
|
|
254
|
+
response = await client.post("/orders", json={"item": "Widget", "email": "a@b.com"})
|
|
255
|
+
assert response.status_code == 201
|
|
256
|
+
|
|
257
|
+
# Background task executes before TestClient returns
|
|
258
|
+
mock_notify.assert_called_once_with("a@b.com", "Order confirmed")
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Prevent Side Effects
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
@pytest.mark.anyio
|
|
265
|
+
async def test_order_no_email(client: AsyncClient, monkeypatch):
|
|
266
|
+
monkeypatch.setattr("app.tasks.send_notification", lambda *a, **kw: None)
|
|
267
|
+
response = await client.post("/orders", json={"item": "Widget", "email": "a@b.com"})
|
|
268
|
+
assert response.status_code == 201
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Note: With `TestClient` (synchronous), background tasks typically execute synchronously before the response returns. With `AsyncClient`, background tasks may or may not complete before the test assertion -- use mocking to control behavior deterministically.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 6. Database Fixtures with aiosqlite
|
|
276
|
+
|
|
277
|
+
### Session-Scoped Engine + Function-Scoped Transactions
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
import pytest
|
|
281
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
282
|
+
from httpx import ASGITransport, AsyncClient
|
|
283
|
+
from app.main import app
|
|
284
|
+
from app.database import get_db, Base
|
|
285
|
+
|
|
286
|
+
@pytest.fixture(scope="session")
|
|
287
|
+
def anyio_backend():
|
|
288
|
+
return "asyncio"
|
|
289
|
+
|
|
290
|
+
@pytest.fixture(scope="session")
|
|
291
|
+
async def engine():
|
|
292
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
293
|
+
async with engine.begin() as conn:
|
|
294
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
295
|
+
yield engine
|
|
296
|
+
await engine.dispose()
|
|
297
|
+
|
|
298
|
+
@pytest.fixture
|
|
299
|
+
async def db_session(engine):
|
|
300
|
+
session_factory = async_sessionmaker(engine, class_=AsyncSession)
|
|
301
|
+
async with session_factory() as session:
|
|
302
|
+
async with session.begin():
|
|
303
|
+
yield session
|
|
304
|
+
await session.rollback()
|
|
305
|
+
|
|
306
|
+
@pytest.fixture
|
|
307
|
+
async def client(db_session):
|
|
308
|
+
async def override_get_db():
|
|
309
|
+
yield db_session
|
|
310
|
+
|
|
311
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
312
|
+
async with AsyncClient(
|
|
313
|
+
transport=ASGITransport(app=app),
|
|
314
|
+
base_url="http://test"
|
|
315
|
+
) as ac:
|
|
316
|
+
yield ac
|
|
317
|
+
app.dependency_overrides.clear()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Seeded Data Fixture
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
@pytest.fixture
|
|
324
|
+
async def seeded_db(db_session):
|
|
325
|
+
"""Pre-populate the database with test data."""
|
|
326
|
+
db_session.add_all([
|
|
327
|
+
Item(name="Widget", price=9.99),
|
|
328
|
+
Item(name="Gadget", price=19.99),
|
|
329
|
+
])
|
|
330
|
+
await db_session.flush()
|
|
331
|
+
return db_session
|
|
332
|
+
|
|
333
|
+
@pytest.mark.anyio
|
|
334
|
+
async def test_list_items(client: AsyncClient, seeded_db):
|
|
335
|
+
response = await client.get("/items/")
|
|
336
|
+
assert response.status_code == 200
|
|
337
|
+
items = response.json()
|
|
338
|
+
assert len(items) == 2
|
|
339
|
+
assert items[0]["name"] == "Widget"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Raw aiosqlite (Without SQLAlchemy)
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
import aiosqlite
|
|
346
|
+
|
|
347
|
+
@pytest.fixture
|
|
348
|
+
async def db():
|
|
349
|
+
async with aiosqlite.connect(":memory:") as db:
|
|
350
|
+
await db.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, price REAL)")
|
|
351
|
+
await db.commit()
|
|
352
|
+
yield db
|
|
353
|
+
|
|
354
|
+
@pytest.fixture
|
|
355
|
+
async def client(db):
|
|
356
|
+
async def override_get_db():
|
|
357
|
+
yield db
|
|
358
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
359
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
|
360
|
+
yield ac
|
|
361
|
+
app.dependency_overrides.clear()
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## 7. Testing Patterns
|
|
367
|
+
|
|
368
|
+
### Error Response Testing
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
@pytest.mark.anyio
|
|
372
|
+
async def test_item_not_found(client: AsyncClient):
|
|
373
|
+
response = await client.get("/items/99999")
|
|
374
|
+
assert response.status_code == 404
|
|
375
|
+
assert response.json()["detail"] == "Item not found"
|
|
376
|
+
|
|
377
|
+
@pytest.mark.anyio
|
|
378
|
+
async def test_validation_error(client: AsyncClient):
|
|
379
|
+
response = await client.post("/items/", json={"name": ""})
|
|
380
|
+
assert response.status_code == 422
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Header and Cookie Testing
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
@pytest.mark.anyio
|
|
387
|
+
async def test_auth_header_required(client: AsyncClient):
|
|
388
|
+
response = await client.get("/protected")
|
|
389
|
+
assert response.status_code == 401
|
|
390
|
+
|
|
391
|
+
@pytest.mark.anyio
|
|
392
|
+
async def test_with_auth(client: AsyncClient):
|
|
393
|
+
response = await client.get(
|
|
394
|
+
"/protected",
|
|
395
|
+
headers={"Authorization": "Bearer test-token"},
|
|
396
|
+
)
|
|
397
|
+
assert response.status_code == 200
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### File Upload Testing
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
@pytest.mark.anyio
|
|
404
|
+
async def test_file_upload(client: AsyncClient):
|
|
405
|
+
response = await client.post(
|
|
406
|
+
"/upload",
|
|
407
|
+
files={"file": ("test.txt", b"file content", "text/plain")},
|
|
408
|
+
)
|
|
409
|
+
assert response.status_code == 200
|
|
410
|
+
assert response.json()["filename"] == "test.txt"
|
|
411
|
+
```
|