@pilllesss/yorn 1.0.182 → 1.0.183

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.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,580 @@
1
+ # FastAPI Code Review Guide
2
+
3
+ > FastAPI code review guide covering dependency injection (`Depends`), Pydantic v2 validation boundaries, async correctness, database session lifecycle and N+1, security, and a test-driven verification workflow that turns the reviewer's in-process test client into a tool for *proving* bugs rather than guessing at them.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Dependency Injection (`Depends`)](#dependency-injection-depends)
8
+ - [Pydantic v2 Models & Validation](#pydantic-v2-models--validation)
9
+ - [Async Correctness](#async-correctness)
10
+ - [Database Sessions & N+1](#database-sessions--n1)
11
+ - [Security](#security)
12
+ - [Test-Driven Verification](#test-driven-verification)
13
+ - [Review Checklist](#review-checklist)
14
+ - [References](#references)
15
+
16
+ ---
17
+
18
+ ## Dependency Injection (`Depends`)
19
+
20
+ FastAPI's `Depends` is the seam that keeps routes thin and testable. Most review problems here come from doing real work in the route function instead of behind a dependency.
21
+
22
+ ### Business logic belongs behind a dependency or service, not in the route
23
+
24
+ ```python
25
+ # ❌ Bad — DB access, auth, and business rules all inline in the route
26
+ @app.get("/orders/{order_id}")
27
+ async def get_order(order_id: int):
28
+ conn = await asyncpg.connect(DATABASE_URL) # connection created per request
29
+ row = await conn.fetchrow("SELECT * FROM orders WHERE id = $1", order_id)
30
+ await conn.close()
31
+ if row is None:
32
+ raise HTTPException(404)
33
+ return dict(row)
34
+
35
+ # ✅ Good — the route declares what it needs; the session is injected and pooled
36
+ async def get_session() -> AsyncIterator[AsyncSession]:
37
+ async with SessionLocal() as session:
38
+ yield session
39
+
40
+ @app.get("/orders/{order_id}", response_model=OrderOut)
41
+ async def get_order(order_id: int, session: AsyncSession = Depends(get_session)):
42
+ order = await session.get(Order, order_id)
43
+ if order is None:
44
+ raise HTTPException(status_code=404, detail="Order not found")
45
+ return order
46
+ ```
47
+
48
+ The injected version is also the version you can override in tests (see [Test-Driven Verification](#test-driven-verification)).
49
+
50
+ ### `yield` dependencies must clean up, and cleanup runs even on error
51
+
52
+ ```python
53
+ # ❌ Bad — no cleanup; the session leaks if the route raises
54
+ async def get_session() -> AsyncSession:
55
+ return SessionLocal()
56
+
57
+ # ✅ Good — the context manager closes the session on success AND on exception
58
+ async def get_session() -> AsyncIterator[AsyncSession]:
59
+ async with SessionLocal() as session:
60
+ yield session
61
+ ```
62
+
63
+ Review point: confirm any `yield` dependency holding a resource (DB session, file handle, lock) releases it through a context manager or `try/finally`, so an exception in the route does not leak it.
64
+
65
+ ### Don't re-create singletons per request
66
+
67
+ ```python
68
+ # ❌ Bad — a new HTTP client (and connection pool) per request
69
+ @app.get("/proxy")
70
+ async def proxy(client: httpx.AsyncClient = Depends(lambda: httpx.AsyncClient())):
71
+ ...
72
+
73
+ # ✅ Good — one client for the app lifetime, injected by reference
74
+ @asynccontextmanager
75
+ async def lifespan(app: FastAPI):
76
+ app.state.http = httpx.AsyncClient()
77
+ yield
78
+ await app.state.http.aclose()
79
+
80
+ def get_http(request: Request) -> httpx.AsyncClient:
81
+ return request.app.state.http
82
+ ```
83
+
84
+ ### Prefer the `Annotated` form and async dependencies
85
+
86
+ Since FastAPI 0.95 the idiomatic way to declare a dependency is `Annotated[T, Depends(...)]`, not the default-value form. It is reusable across routes and plays well with type checkers. Also prefer `async def` dependencies: a sync (`def`) dependency runs in the threadpool, which is wasted overhead for a small non-I/O check.
87
+
88
+ ```python
89
+ # ⚠️ Older form — still works, but not the current idiom
90
+ @app.get("/items")
91
+ async def list_items(session: AsyncSession = Depends(get_session)): ...
92
+
93
+ # ✅ Good — Annotated form; define once, reuse everywhere
94
+ SessionDep = Annotated[AsyncSession, Depends(get_session)]
95
+
96
+ @app.get("/items")
97
+ async def list_items(session: SessionDep): ...
98
+ ```
99
+
100
+ ### Use dependencies to validate existence and permissions — they're cached per request
101
+
102
+ A dependency is the natural place to answer "does this resource exist and may this caller touch it?" Pydantic validates *shape*; a dependency validates against the database. FastAPI caches each dependency's result within a single request, so chaining small dependencies costs nothing extra and removes duplicated lookups.
103
+
104
+ ```python
105
+ # ✅ Good — small dependencies chain; valid_post is resolved once per request
106
+ async def valid_post(post_id: int, session: SessionDep) -> Post:
107
+ post = await session.get(Post, post_id)
108
+ if post is None:
109
+ raise HTTPException(status_code=404, detail="Post not found")
110
+ return post
111
+
112
+ async def owned_post(post: Annotated[Post, Depends(valid_post)], user: CurrentUser) -> Post:
113
+ if post.owner_id != user.id:
114
+ raise HTTPException(status_code=403, detail="Forbidden")
115
+ return post
116
+
117
+ @app.delete("/posts/{post_id}", status_code=204)
118
+ async def delete_post(post: Annotated[Post, Depends(owned_post)], session: SessionDep):
119
+ await session.delete(post) # existence + ownership already enforced
120
+ await session.commit()
121
+ ```
122
+
123
+ This is also the cleanest place to fix the auth-vs-authorization bug from the [Security](#security) section: the ownership check moves into a reusable `owned_post` dependency.
124
+
125
+ ---
126
+
127
+ ## Pydantic v2 Models & Validation
128
+
129
+ ### Separate input and output models; never echo the ORM object directly
130
+
131
+ ```python
132
+ # ❌ Bad — response_model is the DB model, so hashed_password leaks to the client
133
+ @app.post("/users", response_model=UserTable)
134
+ async def create_user(user: UserTable): # also accepts client-set id, is_admin...
135
+ ...
136
+
137
+ # ✅ Good — distinct schemas draw the trust boundary
138
+ class UserCreate(BaseModel):
139
+ email: EmailStr
140
+ password: str
141
+
142
+ class UserOut(BaseModel):
143
+ id: int
144
+ email: EmailStr
145
+ model_config = ConfigDict(from_attributes=True) # read from ORM safely
146
+
147
+ @app.post("/users", response_model=UserOut, status_code=201)
148
+ async def create_user(payload: UserCreate, session: AsyncSession = Depends(get_session)):
149
+ ...
150
+ ```
151
+
152
+ `response_model` is a filter, not just documentation — fields absent from the output model are stripped from the response. Reusing the DB model as the response is the most common way sensitive fields leak.
153
+
154
+ ### Use distinct Create and Update schemas
155
+
156
+ ```python
157
+ # ❌ Bad — one schema for create and update means every field is required on PATCH
158
+ class ItemSchema(BaseModel):
159
+ name: str
160
+ price: float
161
+
162
+ # ✅ Good — update is a partial; create requires the full payload
163
+ class ItemCreate(BaseModel):
164
+ name: str
165
+ price: float = Field(gt=0)
166
+
167
+ class ItemUpdate(BaseModel):
168
+ name: str | None = None
169
+ price: float | None = Field(default=None, gt=0)
170
+ ```
171
+
172
+ ### Validate at the boundary, not after the DB write
173
+
174
+ ```python
175
+ # ❌ Bad — negative quantity reaches the database before anything checks it
176
+ @app.post("/cart")
177
+ async def add_to_cart(item_id: int, quantity: int):
178
+ await save(item_id, quantity) # quantity = -5 silently accepted
179
+
180
+ # ✅ Good — the type system rejects it before the handler body runs
181
+ class CartLine(BaseModel):
182
+ item_id: int
183
+ quantity: int = Field(gt=0)
184
+
185
+ @app.post("/cart")
186
+ async def add_to_cart(line: CartLine):
187
+ await save(line.item_id, line.quantity)
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Async Correctness
193
+
194
+ This is the axis on which FastAPI differs most from Django and Flask, and the one most worth a reviewer's attention. FastAPI's throughput comes from a single event loop interleaving many concurrent requests. That model only holds if the loop is **never blocked**: one synchronous call on the loop stalls *every* in-flight request, not just its own. Get this wrong across the codebase and FastAPI does not just lose its edge — it performs *worse* than a sync framework like Flask, because Flask's worker-per-request model has no shared loop to choke. The reviewer's job is to keep work on the loop genuinely non-blocking and to treat every escape hatch as a cost, not a fix.
195
+
196
+ ### Never call blocking code inside an `async def` route
197
+
198
+ ```python
199
+ # ❌ Bad — blocking I/O on the loop freezes ALL concurrent requests, not just this one
200
+ @app.get("/report")
201
+ async def report():
202
+ data = requests.get("https://slow-api.example.com").json() # blocking socket
203
+ time.sleep(2) # blocks the loop
204
+ return data
205
+
206
+ # ✅ Good — await a native-async client; the loop serves other requests meanwhile
207
+ @app.get("/report")
208
+ async def report(client: httpx.AsyncClient = Depends(get_http)):
209
+ resp = await client.get("https://slow-api.example.com")
210
+ return resp.json()
211
+ ```
212
+
213
+ ### Prefer native-async SDKs over sync libraries
214
+
215
+ The right fix for blocking I/O is almost always a library that speaks `async` natively — not wrapping a sync one. Reach for the async client first; the threadpool is the last resort, not the default.
216
+
217
+ | Sync (blocks the loop) | Native-async replacement |
218
+ |------------------------|--------------------------|
219
+ | `requests` | `httpx.AsyncClient`, `aiohttp` |
220
+ | `psycopg2` (sync) | `asyncpg`, SQLAlchemy async engine |
221
+ | `redis-py` (sync) | `redis.asyncio` |
222
+ | `pymongo` | `motor` |
223
+ | `boto3` | `aioboto3` |
224
+
225
+ If you find `asyncio.run(...)`, a new event loop, or a manually started thread *inside* a route, that is a red flag — it's an attempt to bolt sync code onto the loop. `asyncio.run()` inside a running loop raises `RuntimeError` outright; the rest quietly burns the performance you adopted FastAPI for.
226
+
227
+ ```python
228
+ # ❌ Bad — spinning up a loop/thread to call an async SDK from a sync context
229
+ @app.get("/users/{uid}")
230
+ def get_user(uid: int):
231
+ return asyncio.run(repo.fetch(uid)) # RuntimeError under the running loop
232
+
233
+ # ✅ Good — let the route be async and await the native client directly
234
+ @app.get("/users/{uid}")
235
+ async def get_user(uid: int):
236
+ return await repo.fetch(uid)
237
+ ```
238
+
239
+ ### The threadpool is a bounded escape hatch, not a default
240
+
241
+ A plain `def` route — and `run_in_threadpool(...)` — does not run on the loop; FastAPI runs it in a **bounded** worker threadpool (AnyIO's default cap is 40 threads). For an occasional, genuinely-unavoidable blocking call this is the correct tool:
242
+
243
+ ```python
244
+ from fastapi.concurrency import run_in_threadpool
245
+
246
+ @app.get("/legacy")
247
+ async def legacy():
248
+ return await run_in_threadpool(blocking_library_call) # only if no async SDK exists
249
+ ```
250
+
251
+ But it does not scale the way the loop does. Route every hot path through the threadpool and, under load, all workers block at once; further requests queue behind the cap and throughput collapses. Spawning your own threads or processes to "add concurrency" makes it worse: once live threads exceed the machine's core count, context-switch and GIL contention degrade performance sharply rather than improving it. The escape hatch is for the rare blocking dependency you cannot replace — not a substitute for choosing async SDKs.
252
+
253
+ Review heuristic: a `def` route is acceptable for a low-traffic endpoint with no async equivalent. A high-traffic endpoint doing blocking work in a `def` route (or via `run_in_threadpool`) is a scaling bug — flag it and ask for an async SDK.
254
+
255
+ ### CPU-bound work belongs in a worker process, not the loop or the threadpool
256
+
257
+ Neither the event loop nor the threadpool helps CPU-bound work: under the GIL only one thread runs Python bytecode at a time, so a heavy computation blocks just as badly from a threadpool as from the loop. Offload it to a separate process (Celery, Arq, RQ, or `multiprocessing`).
258
+
259
+ ```python
260
+ # ❌ Bad — a CPU-heavy job pins a worker; throughput drops for everyone
261
+ @app.post("/render")
262
+ async def render(doc: Doc):
263
+ return heavy_pdf_render(doc) # seconds of pure CPU on the loop
264
+
265
+ # ✅ Good — enqueue to a worker process; return a job handle
266
+ @app.post("/render", status_code=202)
267
+ async def render(doc: Doc):
268
+ job = await queue.enqueue(heavy_pdf_render, doc)
269
+ return {"job_id": job.id}
270
+ ```
271
+
272
+ ### Don't fire-and-forget unawaited coroutines
273
+
274
+ ```python
275
+ # ❌ Bad — coroutine never awaited; the email is never sent (and no error surfaces)
276
+ @app.post("/signup")
277
+ async def signup(user: UserCreate):
278
+ send_welcome_email(user.email) # returns a coroutine, silently dropped
279
+
280
+ # ✅ Good — defer post-response work with BackgroundTasks
281
+ @app.post("/signup")
282
+ async def signup(user: UserCreate, tasks: BackgroundTasks):
283
+ tasks.add_task(send_welcome_email, user.email)
284
+ ```
285
+
286
+ `BackgroundTasks` runs in-process and offers no retries or persistence — use it only for short, fire-and-forget work (send an email, log an event). Anything long-running or retry-critical (data processing, payments) belongs in a real task queue (Celery/Arq/RQ).
287
+
288
+ ---
289
+
290
+ ## Database Sessions & N+1
291
+
292
+ > 📖 For cross-language N+1 patterns and solutions, see [N+1 Queries Guide](cross-cutting/n-plus-one-queries.md)
293
+
294
+ ### One session per request, injected — not a global
295
+
296
+ ```python
297
+ # ❌ Bad — a module-level session is shared across concurrent requests (not safe)
298
+ session = SessionLocal()
299
+
300
+ # ✅ Good — request-scoped session via dependency (see get_session above)
301
+ @app.get("/items")
302
+ async def list_items(session: AsyncSession = Depends(get_session)):
303
+ ...
304
+ ```
305
+
306
+ ### Eager-load relationships to avoid N+1
307
+
308
+ ```python
309
+ # ❌ Bad — one query for orders, then one query per order for its customer
310
+ orders = (await session.execute(select(Order))).scalars().all()
311
+ return [{"id": o.id, "customer": o.customer.name} for o in orders] # N+1
312
+
313
+ # ✅ Good — a single query with the relationship eager-loaded
314
+ stmt = select(Order).options(selectinload(Order.customer))
315
+ orders = (await session.execute(stmt)).scalars().all()
316
+ return [{"id": o.id, "customer": o.customer.name} for o in orders]
317
+ ```
318
+
319
+ With async SQLAlchemy, lazy attribute access outside the session often raises instead of silently querying — but the design issue is the same. Look for relationship access inside a loop without an `options(...)` eager load.
320
+
321
+ ### Paginate list endpoints
322
+
323
+ ```python
324
+ # ❌ Bad — returns every row; degrades as the table grows
325
+ @app.get("/users")
326
+ async def list_users(session: AsyncSession = Depends(get_session)):
327
+ return (await session.execute(select(User))).scalars().all()
328
+
329
+ # ✅ Good — bounded page with a sane cap
330
+ @app.get("/users", response_model=list[UserOut])
331
+ async def list_users(
332
+ session: AsyncSession = Depends(get_session),
333
+ limit: int = Query(default=50, le=100),
334
+ offset: int = Query(default=0, ge=0),
335
+ ):
336
+ stmt = select(User).limit(limit).offset(offset)
337
+ return (await session.execute(stmt)).scalars().all()
338
+ ```
339
+
340
+ ### Aggregate and join in SQL, not in Python
341
+
342
+ If a handler pulls rows into memory and then loops to group, count, or join them, the database is being used as dumb storage. Push the work down — the database does set operations far faster, and you transfer less data.
343
+
344
+ ```python
345
+ # ❌ Bad — fetch every order, then tally per customer in Python
346
+ orders = (await session.execute(select(Order))).scalars().all()
347
+ totals: dict[int, float] = {}
348
+ for o in orders:
349
+ totals[o.customer_id] = totals.get(o.customer_id, 0) + o.amount
350
+
351
+ # ✅ Good — let the database group and sum
352
+ stmt = select(Order.customer_id, func.sum(Order.amount)).group_by(Order.customer_id)
353
+ totals = dict((await session.execute(stmt)).all())
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Security
359
+
360
+ ### A declared auth dependency is not an enforced authorization check
361
+
362
+ This is the highest-value thing to look for. `Depends(get_current_user)` proves *who* the caller is — it does **not** prove they may touch *this* resource.
363
+
364
+ ```python
365
+ # ❌ Bad — any authenticated user can delete any other user's document
366
+ @app.delete("/documents/{doc_id}")
367
+ async def delete_document(
368
+ doc_id: int,
369
+ user: User = Depends(get_current_user),
370
+ session: AsyncSession = Depends(get_session),
371
+ ):
372
+ doc = await session.get(Document, doc_id)
373
+ await session.delete(doc) # never checks doc.owner_id == user.id
374
+ await session.commit()
375
+
376
+ # ✅ Good — ownership is verified before the mutation
377
+ @app.delete("/documents/{doc_id}", status_code=204)
378
+ async def delete_document(
379
+ doc_id: int,
380
+ user: User = Depends(get_current_user),
381
+ session: AsyncSession = Depends(get_session),
382
+ ):
383
+ doc = await session.get(Document, doc_id)
384
+ if doc is None:
385
+ raise HTTPException(status_code=404, detail="Not found")
386
+ if doc.owner_id != user.id:
387
+ raise HTTPException(status_code=403, detail="Forbidden")
388
+ await session.delete(doc)
389
+ await session.commit()
390
+ ```
391
+
392
+ The [Test-Driven Verification](#test-driven-verification) section reproduces exactly this bug with a failing test.
393
+
394
+ ### Parameterize SQL; never f-string user input
395
+
396
+ > **跨语言 SQL 注入防护详见 [SQL Injection Prevention Guide](cross-cutting/sql-injection-prevention.md)**,含 Python/Java/Go/Node.js/PHP/C# 示例及 ORM 不安全用法。
397
+
398
+ ### Don't widen CORS to credentials + wildcard
399
+
400
+ ```python
401
+ # ❌ Bad — wildcard origin together with credentials is rejected by browsers and unsafe
402
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)
403
+
404
+ # ✅ Good — enumerate trusted origins when credentials are allowed
405
+ app.add_middleware(
406
+ CORSMiddleware,
407
+ allow_origins=["https://app.example.com"],
408
+ allow_credentials=True,
409
+ )
410
+ ```
411
+
412
+ Also check: secrets read from config/env (not hard-coded), `HTTPException` details that don't leak internals (stack traces, SQL), and rate limiting on auth endpoints.
413
+
414
+ ---
415
+
416
+ ## Test-Driven Verification
417
+
418
+ > Inspired by the test-driven development discipline: *if you didn't watch the test fail, you don't know it tests the right thing.* This matters even more for a coding agent than for a human reviewer. An agent's reading and reasoning are fallible — it can misread control flow, hallucinate a guarantee that isn't there, or rationalize a comfortable conclusion — so a prose verdict like "this looks safe" carries little weight on its own. An executable test is the one piece of **objective ground truth** the agent fully controls: it either passes or it doesn't, regardless of how confident the reasoning felt. That is what makes tests the agent's anchor of confidence. Reviewing the same way the discipline writes code — reproduce, don't assert — turns a hunch into proof.
419
+
420
+ A natural-language review comment ("this might let users delete each other's data") is exactly that kind of fallible hypothesis. FastAPI makes the ground truth cheap to obtain: an in-process client (`httpx.AsyncClient` over `ASGITransport`) runs the whole app, and `app.dependency_overrides` swaps out auth and the database without patching internals. So instead of trusting its own read of the code, the agent settles the question by reproduction.
421
+
422
+ ### Reproduce a suspected bug with a failing test (Verify RED)
423
+
424
+ Suppose the reviewer suspects the `DELETE /documents/{doc_id}` route above never checks ownership. Write the test that asserts the *secure* behavior, then run it and **watch it fail** — the failure is the proof.
425
+
426
+ ```python
427
+ # test_document_authorization.py
428
+ import pytest
429
+ from httpx import AsyncClient, ASGITransport
430
+ from fastapi import Header
431
+ from app.main import app
432
+ from app.deps import get_current_user, get_session
433
+
434
+ # Two users; the override picks one based on a test header.
435
+ USERS = {"alice": User(id=1, email="alice@example.com"),
436
+ "bob": User(id=2, email="bob@example.com")}
437
+
438
+ def fake_current_user(x_test_user: str = Header(default="alice")) -> User:
439
+ return USERS[x_test_user]
440
+
441
+ @pytest.mark.asyncio
442
+ async def test_user_cannot_delete_another_users_document(session): # async fixture
443
+ # Arrange: a document owned by Alice (id=1)
444
+ session.add(Document(id=10, owner_id=1, title="Alice's doc"))
445
+ await session.commit()
446
+
447
+ app.dependency_overrides[get_current_user] = fake_current_user
448
+ app.dependency_overrides[get_session] = lambda: session
449
+
450
+ # Act: Bob tries to delete Alice's document
451
+ transport = ASGITransport(app=app)
452
+ async with AsyncClient(transport=transport, base_url="http://test") as client:
453
+ resp = await client.delete("/documents/10", headers={"X-Test-User": "bob"})
454
+
455
+ # Assert the SECURE behavior we expect
456
+ assert resp.status_code == 403
457
+
458
+ app.dependency_overrides.clear()
459
+ ```
460
+
461
+ Run it against the unfixed code and confirm the failure is the bug, not a typo:
462
+
463
+ ```bash
464
+ $ pytest test_document_authorization.py
465
+ FAILED assert 204 == 403
466
+ # ^ the endpoint deleted Alice's document for Bob — vulnerability confirmed
467
+ ```
468
+
469
+ A failure of `204 == 403` (not an import error, not a 404) is what makes the finding credible: the route returned success for an action that should have been forbidden. Now the fix from the [Security](#security) section turns it green:
470
+
471
+ ```bash
472
+ $ pytest test_document_authorization.py
473
+ PASSED
474
+ ```
475
+
476
+ Attach this test to the review. It documents the vulnerability, proves the fix, and guards against regression — far stronger than "consider checking ownership here."
477
+
478
+ ### Prefer `dependency_overrides` over `patch`/`mock`
479
+
480
+ FastAPI's DI is the seam the TDD discipline asks for: when something is hard to test without mocking everything, that usually signals coupling — and `Depends` already gives you the injection point, so you rarely need `unittest.mock.patch`.
481
+
482
+ ```python
483
+ # ❌ Bad — patching internals: brittle, couples the test to import paths
484
+ @patch("app.routes.orders.asyncpg.connect")
485
+ def test_get_order(mock_connect): ...
486
+
487
+ # ✅ Good — override the dependency with a real in-memory fake
488
+ app.dependency_overrides[get_session] = lambda: in_memory_session
489
+ app.dependency_overrides[get_current_user] = lambda: test_user
490
+ ```
491
+
492
+ Always reset overrides between tests (`app.dependency_overrides.clear()` in a fixture teardown) so state doesn't leak across tests.
493
+
494
+ The reproduction above uses `httpx.AsyncClient` over `ASGITransport` with `@pytest.mark.asyncio` — the community convention for an async app, so the suite shares the app's event loop and you avoid loop-mismatch errors later. The synchronous `TestClient` is simpler and fine for a fully sync app, but standardizing on the async client from the start saves a painful migration once any route or fixture becomes async.
495
+
496
+ ### Critique the PR's own tests, not just its source
497
+
498
+ A PR that ships tests is not automatically safe. Apply these checks to the *tests* in the diff:
499
+
500
+ ```python
501
+ # ❌ Bad — happy-path only. Proves the route works when everything is correct,
502
+ # says nothing about the validation and authorization paths.
503
+ def test_create_item():
504
+ resp = client.post("/items", json={"name": "x", "price": 5})
505
+ assert resp.status_code == 201
506
+
507
+ # ✅ Good — the boundary and failure paths are where bugs live
508
+ def test_create_item_rejects_negative_price():
509
+ resp = client.post("/items", json={"name": "x", "price": -5})
510
+ assert resp.status_code == 422
511
+
512
+ def test_create_item_requires_authentication():
513
+ resp = client_without_auth.post("/items", json={"name": "x", "price": 5})
514
+ assert resp.status_code == 401
515
+ ```
516
+
517
+ Review questions for the test suite:
518
+
519
+ - **Does it test behavior, or the mock?** An assertion that only confirms a mock was called proves the test's own setup, not the endpoint.
520
+ - **Are the failure paths covered?** 401/403/404/422 — not just 200/201. Bugs cluster at the boundaries.
521
+ - **Is the mock complete?** A partial mock of an external API response that omits fields the handler reads passes in the test and fails in production.
522
+ - **Were the tests written after the fact?** Tests added alongside an implementation and passing on the first run never demonstrated that they can fail — and so prove little. A test that reproduces the bug (fails first, then passes) is worth more than one that was green from birth.
523
+
524
+ ---
525
+
526
+ ## Review Checklist
527
+
528
+ ### Dependency Injection
529
+
530
+ - [ ] Routes stay thin — DB access and business rules live behind `Depends`/services
531
+ - [ ] `yield` dependencies release resources via context manager or `try/finally`
532
+ - [ ] Singletons (HTTP clients, pools) created once in `lifespan`, not per request
533
+ - [ ] `Annotated[T, Depends(...)]` form used; dependencies are `async def` unless they do blocking I/O
534
+ - [ ] Existence/permission checks live in (cached) dependencies, not copy-pasted into routes
535
+ - [ ] Dependencies are overridable in tests (no resources created inline in the route)
536
+
537
+ ### Validation
538
+
539
+ - [ ] Input and output use distinct Pydantic models; ORM objects are not the `response_model`
540
+ - [ ] `response_model` set so sensitive fields can't leak
541
+ - [ ] Separate Create vs Update schemas (update is partial)
542
+ - [ ] Constraints (`gt`, `le`, `EmailStr`, ...) enforced at the boundary, before the DB write
543
+
544
+ ### Async
545
+
546
+ - [ ] No blocking calls (`requests`, `time.sleep`, blocking DB drivers) inside `async def`
547
+ - [ ] Native-async SDKs preferred (`httpx`, `asyncpg`, `redis.asyncio`, ...) over sync ones
548
+ - [ ] No `asyncio.run`/manual event loops/manual threads inside routes
549
+ - [ ] `run_in_threadpool`/`def` routes used only as a last resort, not on hot paths
550
+ - [ ] CPU-bound work offloaded to a worker process (Celery/Arq/RQ), not the loop or threadpool
551
+ - [ ] No unawaited coroutines; `BackgroundTasks` only for short fire-and-forget work
552
+
553
+ ### Database
554
+
555
+ - [ ] One request-scoped session via dependency; no module-level shared session
556
+ - [ ] Relationships eager-loaded (`selectinload`/`joinedload`) where accessed in a loop
557
+ - [ ] Joins/aggregations done in SQL, not by looping in Python
558
+ - [ ] List endpoints are paginated with a capped `limit`
559
+
560
+ ### Security
561
+
562
+ - [ ] Authentication dependency is backed by an explicit **authorization** check (ownership/role)
563
+ - [ ] All SQL parameterized; no f-string interpolation of user input
564
+ - [ ] CORS does not combine `allow_origins=["*"]` with `allow_credentials=True`
565
+ - [ ] Secrets come from config/env; error responses don't leak internals
566
+
567
+ ### Tests
568
+
569
+ - [ ] Suspected bugs reproduced with a failing test (`TestClient`/`AsyncClient`) before being claimed
570
+ - [ ] `dependency_overrides` used instead of patching internals; overrides reset between tests
571
+ - [ ] Failure paths covered (401/403/404/422), not just the happy path
572
+ - [ ] Mocks of external responses are complete, not partial
573
+ - [ ] New tests demonstrate they can fail (reproduce-then-fix), not green from birth
574
+
575
+ ---
576
+
577
+ ## References
578
+
579
+ - [FastAPI official documentation](https://fastapi.tiangolo.com/) — async, dependencies, testing
580
+ - [zhanymkanov/fastapi-best-practices](https://github.com/zhanymkanov/fastapi-best-practices) — production conventions (async routes, dependency caching, project structure)