@malamute/ai-rules 1.0.0 → 1.3.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.
Files changed (145) hide show
  1. package/README.md +272 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/CLAUDE.md +52 -149
  4. package/configs/_shared/rules/conventions/documentation.md +324 -0
  5. package/configs/_shared/rules/conventions/git.md +265 -0
  6. package/configs/_shared/rules/conventions/npm.md +80 -0
  7. package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
  8. package/configs/_shared/rules/conventions/principles.md +334 -0
  9. package/configs/_shared/rules/devops/ci-cd.md +262 -0
  10. package/configs/_shared/rules/devops/docker.md +275 -0
  11. package/configs/_shared/rules/devops/nx.md +194 -0
  12. package/configs/_shared/rules/domain/backend/api-design.md +203 -0
  13. package/configs/_shared/rules/lang/csharp/async.md +220 -0
  14. package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
  15. package/configs/_shared/rules/lang/csharp/linq.md +210 -0
  16. package/configs/_shared/rules/lang/python/async.md +337 -0
  17. package/configs/_shared/rules/lang/python/celery.md +476 -0
  18. package/configs/_shared/rules/lang/python/config.md +339 -0
  19. package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
  20. package/configs/_shared/rules/lang/python/deployment.md +523 -0
  21. package/configs/_shared/rules/lang/python/error-handling.md +330 -0
  22. package/configs/_shared/rules/lang/python/migrations.md +421 -0
  23. package/configs/_shared/rules/lang/python/python.md +172 -0
  24. package/configs/_shared/rules/lang/python/repository.md +383 -0
  25. package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
  26. package/configs/_shared/rules/lang/typescript/async.md +447 -0
  27. package/configs/_shared/rules/lang/typescript/generics.md +356 -0
  28. package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
  29. package/configs/_shared/rules/quality/error-handling.md +48 -0
  30. package/configs/_shared/rules/quality/logging.md +45 -0
  31. package/configs/_shared/rules/quality/observability.md +240 -0
  32. package/configs/_shared/rules/quality/testing-patterns.md +65 -0
  33. package/configs/_shared/rules/security/secrets-management.md +222 -0
  34. package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
  35. package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
  36. package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
  37. package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  38. package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  39. package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  40. package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
  41. package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
  42. package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
  43. package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
  44. package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
  45. package/configs/angular/CLAUDE.md +24 -216
  46. package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
  47. package/configs/angular/rules/core/resource.md +285 -0
  48. package/configs/angular/rules/core/signals.md +323 -0
  49. package/configs/angular/rules/http.md +338 -0
  50. package/configs/angular/rules/routing.md +291 -0
  51. package/configs/angular/rules/ssr.md +312 -0
  52. package/configs/angular/rules/state/signal-store.md +408 -0
  53. package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
  54. package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
  55. package/configs/angular/rules/ui/aria.md +422 -0
  56. package/configs/angular/rules/ui/forms.md +424 -0
  57. package/configs/angular/rules/ui/pipes-directives.md +335 -0
  58. package/configs/angular/{.claude/settings.json → settings.json} +3 -0
  59. package/configs/dotnet/CLAUDE.md +53 -286
  60. package/configs/dotnet/rules/background-services.md +552 -0
  61. package/configs/dotnet/rules/configuration.md +426 -0
  62. package/configs/dotnet/rules/ddd.md +447 -0
  63. package/configs/dotnet/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/rules/mediatr.md +320 -0
  65. package/configs/dotnet/rules/middleware.md +489 -0
  66. package/configs/dotnet/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/rules/validation.md +388 -0
  68. package/configs/dotnet/settings.json +29 -0
  69. package/configs/fastapi/CLAUDE.md +144 -0
  70. package/configs/fastapi/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/rules/dependencies.md +170 -0
  72. package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
  73. package/configs/fastapi/rules/lifespan.md +274 -0
  74. package/configs/fastapi/rules/middleware.md +229 -0
  75. package/configs/fastapi/rules/pydantic.md +433 -0
  76. package/configs/fastapi/rules/responses.md +251 -0
  77. package/configs/fastapi/rules/routers.md +202 -0
  78. package/configs/fastapi/rules/security.md +222 -0
  79. package/configs/fastapi/rules/testing.md +251 -0
  80. package/configs/fastapi/rules/websockets.md +298 -0
  81. package/configs/fastapi/settings.json +35 -0
  82. package/configs/flask/CLAUDE.md +166 -0
  83. package/configs/flask/rules/blueprints.md +208 -0
  84. package/configs/flask/rules/cli.md +285 -0
  85. package/configs/flask/rules/configuration.md +281 -0
  86. package/configs/flask/rules/context.md +238 -0
  87. package/configs/flask/rules/error-handlers.md +278 -0
  88. package/configs/flask/rules/extensions.md +278 -0
  89. package/configs/flask/rules/flask.md +171 -0
  90. package/configs/flask/rules/marshmallow.md +206 -0
  91. package/configs/flask/rules/security.md +267 -0
  92. package/configs/flask/rules/testing.md +284 -0
  93. package/configs/flask/settings.json +35 -0
  94. package/configs/nestjs/CLAUDE.md +57 -215
  95. package/configs/nestjs/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/rules/filters.md +376 -0
  97. package/configs/nestjs/rules/interceptors.md +317 -0
  98. package/configs/nestjs/rules/middleware.md +321 -0
  99. package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
  100. package/configs/nestjs/rules/pipes.md +351 -0
  101. package/configs/nestjs/rules/websockets.md +451 -0
  102. package/configs/nestjs/settings.json +31 -0
  103. package/configs/nextjs/CLAUDE.md +69 -331
  104. package/configs/nextjs/rules/api-routes.md +358 -0
  105. package/configs/nextjs/rules/authentication.md +355 -0
  106. package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
  107. package/configs/nextjs/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/rules/database.md +400 -0
  109. package/configs/nextjs/rules/middleware.md +303 -0
  110. package/configs/nextjs/rules/routing.md +324 -0
  111. package/configs/nextjs/rules/seo.md +350 -0
  112. package/configs/nextjs/rules/server-actions.md +353 -0
  113. package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
  114. package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
  115. package/package.json +24 -9
  116. package/src/cli.js +218 -0
  117. package/src/config.js +63 -0
  118. package/src/index.js +4 -0
  119. package/src/installer.js +414 -0
  120. package/src/merge.js +109 -0
  121. package/src/tech-config.json +45 -0
  122. package/src/utils.js +88 -0
  123. package/configs/dotnet/.claude/settings.json +0 -9
  124. package/configs/nestjs/.claude/settings.json +0 -15
  125. package/configs/python/.claude/rules/flask.md +0 -332
  126. package/configs/python/.claude/settings.json +0 -18
  127. package/configs/python/CLAUDE.md +0 -273
  128. package/src/install.js +0 -315
  129. /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
  130. /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
  131. /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
  132. /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
  133. /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
  134. /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
  135. /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
  136. /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
  137. /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
  138. /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
  139. /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
  140. /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
  141. /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
  142. /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
  143. /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
  144. /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
  145. /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
@@ -0,0 +1,202 @@
1
+ ---
2
+ paths:
3
+ - "**/routers/**/*.py"
4
+ - "**/routes/**/*.py"
5
+ - "**/api/**/*.py"
6
+ - "**/endpoints/**/*.py"
7
+ ---
8
+
9
+ # FastAPI Router Patterns
10
+
11
+ ## Router Organization
12
+
13
+ ```python
14
+ # GOOD - organized router structure
15
+ # app/users/router.py
16
+ from fastapi import APIRouter, status
17
+
18
+ router = APIRouter(
19
+ prefix="/users",
20
+ tags=["Users"],
21
+ responses={404: {"description": "User not found"}},
22
+ )
23
+
24
+ @router.get("/", response_model=list[UserResponse])
25
+ async def list_users(db: DbSession) -> list[User]:
26
+ """List all active users."""
27
+ return await db.scalars(select(User).where(User.is_active))
28
+
29
+ @router.post("/", status_code=status.HTTP_201_CREATED)
30
+ async def create_user(data: UserCreate, db: DbSession) -> UserResponse:
31
+ """Create a new user."""
32
+ user = User(**data.model_dump())
33
+ db.add(user)
34
+ await db.commit()
35
+ return user
36
+
37
+ @router.get("/{user_id}")
38
+ async def get_user(user_id: int, db: DbSession) -> UserResponse:
39
+ """Get user by ID."""
40
+ user = await db.get(User, user_id)
41
+ if not user:
42
+ raise HTTPException(404, "User not found")
43
+ return user
44
+ ```
45
+
46
+ ## Router Registration
47
+
48
+ ```python
49
+ # app/main.py
50
+ from fastapi import FastAPI
51
+ from app.users.router import router as users_router
52
+ from app.auth.router import router as auth_router
53
+ from app.products.router import router as products_router
54
+
55
+ app = FastAPI(title="My API", version="1.0.0")
56
+
57
+ # Version prefix
58
+ api_v1 = APIRouter(prefix="/api/v1")
59
+ api_v1.include_router(users_router)
60
+ api_v1.include_router(auth_router)
61
+ api_v1.include_router(products_router)
62
+
63
+ app.include_router(api_v1)
64
+ ```
65
+
66
+ ## Path Parameters
67
+
68
+ ```python
69
+ # GOOD - typed path parameters with validation
70
+ @router.get("/{user_id}")
71
+ async def get_user(
72
+ user_id: Annotated[int, Path(ge=1, description="User ID")],
73
+ ) -> UserResponse:
74
+ ...
75
+
76
+ # UUID path parameter
77
+ @router.get("/{item_id}")
78
+ async def get_item(item_id: UUID) -> ItemResponse:
79
+ ...
80
+
81
+ # Multiple path parameters
82
+ @router.get("/{user_id}/orders/{order_id}")
83
+ async def get_user_order(user_id: int, order_id: int) -> OrderResponse:
84
+ ...
85
+ ```
86
+
87
+ ## Query Parameters
88
+
89
+ ```python
90
+ # GOOD - query parameters with defaults and validation
91
+ @router.get("/")
92
+ async def list_items(
93
+ skip: int = Query(0, ge=0),
94
+ limit: int = Query(20, ge=1, le=100),
95
+ search: str | None = Query(None, min_length=1, max_length=100),
96
+ status: ItemStatus | None = None,
97
+ sort_by: Literal["name", "created_at", "price"] = "created_at",
98
+ order: Literal["asc", "desc"] = "desc",
99
+ ) -> Page[ItemResponse]:
100
+ ...
101
+
102
+ # List query parameter
103
+ @router.get("/")
104
+ async def get_items(
105
+ ids: Annotated[list[int], Query()] = [],
106
+ ) -> list[ItemResponse]:
107
+ ...
108
+ ```
109
+
110
+ ## Request Body
111
+
112
+ ```python
113
+ # GOOD - explicit body parameter
114
+ @router.post("/")
115
+ async def create_item(
116
+ item: Annotated[ItemCreate, Body(embed=True)],
117
+ ) -> ItemResponse:
118
+ ...
119
+
120
+ # Multiple body parameters
121
+ @router.post("/transfer")
122
+ async def transfer(
123
+ source: Annotated[Account, Body()],
124
+ destination: Annotated[Account, Body()],
125
+ amount: Annotated[Decimal, Body(gt=0)],
126
+ ) -> TransferResult:
127
+ ...
128
+ ```
129
+
130
+ ## Response Configuration
131
+
132
+ ```python
133
+ # GOOD - explicit response models and status codes
134
+ @router.post(
135
+ "/",
136
+ response_model=UserResponse,
137
+ status_code=status.HTTP_201_CREATED,
138
+ responses={
139
+ 201: {"description": "User created successfully"},
140
+ 400: {"model": ErrorResponse, "description": "Validation error"},
141
+ 409: {"model": ErrorResponse, "description": "Email already exists"},
142
+ },
143
+ )
144
+ async def create_user(data: UserCreate) -> User:
145
+ ...
146
+
147
+ # Exclude fields from response
148
+ @router.get("/", response_model_exclude={"password", "secret"})
149
+ async def get_user() -> User:
150
+ ...
151
+
152
+ # Dynamic response model
153
+ @router.get("/", response_model=UserResponse | AdminResponse)
154
+ async def get_current(user: CurrentUser) -> User:
155
+ return user
156
+ ```
157
+
158
+ ## File Uploads
159
+
160
+ ```python
161
+ from fastapi import File, UploadFile
162
+
163
+ @router.post("/upload")
164
+ async def upload_file(
165
+ file: Annotated[UploadFile, File(description="File to upload")],
166
+ ) -> dict:
167
+ contents = await file.read()
168
+ return {"filename": file.filename, "size": len(contents)}
169
+
170
+ # Multiple files
171
+ @router.post("/upload-many")
172
+ async def upload_files(
173
+ files: Annotated[list[UploadFile], File()],
174
+ ) -> list[dict]:
175
+ return [{"filename": f.filename} for f in files]
176
+ ```
177
+
178
+ ## Form Data
179
+
180
+ ```python
181
+ from fastapi import Form
182
+
183
+ @router.post("/login")
184
+ async def login(
185
+ username: Annotated[str, Form()],
186
+ password: Annotated[str, Form()],
187
+ ) -> Token:
188
+ ...
189
+ ```
190
+
191
+ ## Headers and Cookies
192
+
193
+ ```python
194
+ from fastapi import Header, Cookie
195
+
196
+ @router.get("/")
197
+ async def get_items(
198
+ x_token: Annotated[str, Header()],
199
+ session_id: Annotated[str | None, Cookie()] = None,
200
+ ) -> list[Item]:
201
+ ...
202
+ ```
@@ -0,0 +1,222 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Security Patterns
7
+
8
+ ## OAuth2 Password Flow
9
+
10
+ ```python
11
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
12
+
13
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
14
+
15
+ @router.post("/token")
16
+ async def login(
17
+ form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
18
+ db: DbSession,
19
+ ) -> Token:
20
+ user = await authenticate_user(db, form_data.username, form_data.password)
21
+ if not user:
22
+ raise HTTPException(
23
+ status_code=401,
24
+ detail="Incorrect username or password",
25
+ headers={"WWW-Authenticate": "Bearer"},
26
+ )
27
+
28
+ access_token = create_access_token(data={"sub": str(user.id)})
29
+ return Token(access_token=access_token, token_type="bearer")
30
+ ```
31
+
32
+ ## JWT Authentication
33
+
34
+ ```python
35
+ from jose import JWTError, jwt
36
+ from datetime import datetime, timedelta
37
+
38
+ SECRET_KEY = settings.secret_key
39
+ ALGORITHM = "HS256"
40
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
41
+
42
+ def create_access_token(data: dict) -> str:
43
+ to_encode = data.copy()
44
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
45
+ to_encode.update({"exp": expire})
46
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
47
+
48
+ async def get_current_user(
49
+ token: Annotated[str, Depends(oauth2_scheme)],
50
+ db: DbSession,
51
+ ) -> User:
52
+ credentials_exception = HTTPException(
53
+ status_code=401,
54
+ detail="Could not validate credentials",
55
+ headers={"WWW-Authenticate": "Bearer"},
56
+ )
57
+
58
+ try:
59
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
60
+ user_id: str = payload.get("sub")
61
+ if user_id is None:
62
+ raise credentials_exception
63
+ except JWTError:
64
+ raise credentials_exception
65
+
66
+ user = await db.get(User, int(user_id))
67
+ if user is None:
68
+ raise credentials_exception
69
+
70
+ return user
71
+
72
+ CurrentUser = Annotated[User, Depends(get_current_user)]
73
+ ```
74
+
75
+ ## API Key Authentication
76
+
77
+ ```python
78
+ from fastapi.security import APIKeyHeader, APIKeyQuery
79
+
80
+ api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
81
+ api_key_query = APIKeyQuery(name="api_key", auto_error=False)
82
+
83
+ async def get_api_key(
84
+ api_key_header: str | None = Depends(api_key_header),
85
+ api_key_query: str | None = Depends(api_key_query),
86
+ ) -> str:
87
+ api_key = api_key_header or api_key_query
88
+ if not api_key:
89
+ raise HTTPException(403, "API key required")
90
+
91
+ if not await verify_api_key(api_key):
92
+ raise HTTPException(403, "Invalid API key")
93
+
94
+ return api_key
95
+
96
+ @router.get("/data", dependencies=[Depends(get_api_key)])
97
+ async def get_data() -> dict:
98
+ ...
99
+ ```
100
+
101
+ ## Role-Based Access Control
102
+
103
+ ```python
104
+ from enum import Enum
105
+ from functools import wraps
106
+
107
+ class Role(str, Enum):
108
+ USER = "user"
109
+ ADMIN = "admin"
110
+ MODERATOR = "moderator"
111
+
112
+ def require_roles(*roles: Role):
113
+ async def dependency(user: CurrentUser) -> User:
114
+ if user.role not in roles:
115
+ raise HTTPException(
116
+ status_code=403,
117
+ detail=f"Required roles: {', '.join(r.value for r in roles)}",
118
+ )
119
+ return user
120
+ return dependency
121
+
122
+ AdminOnly = Annotated[User, Depends(require_roles(Role.ADMIN))]
123
+ ModeratorOrAdmin = Annotated[User, Depends(require_roles(Role.ADMIN, Role.MODERATOR))]
124
+
125
+ @router.delete("/{user_id}")
126
+ async def delete_user(user_id: int, admin: AdminOnly) -> None:
127
+ ...
128
+ ```
129
+
130
+ ## Permission-Based Access
131
+
132
+ ```python
133
+ class Permission(str, Enum):
134
+ READ_USERS = "read:users"
135
+ WRITE_USERS = "write:users"
136
+ DELETE_USERS = "delete:users"
137
+
138
+ def require_permissions(*permissions: Permission):
139
+ async def dependency(user: CurrentUser) -> User:
140
+ user_permissions = set(user.permissions)
141
+ required = set(permissions)
142
+
143
+ if not required.issubset(user_permissions):
144
+ missing = required - user_permissions
145
+ raise HTTPException(
146
+ status_code=403,
147
+ detail=f"Missing permissions: {', '.join(p.value for p in missing)}",
148
+ )
149
+ return user
150
+ return dependency
151
+
152
+ CanDeleteUsers = Annotated[User, Depends(require_permissions(Permission.DELETE_USERS))]
153
+ ```
154
+
155
+ ## Password Hashing
156
+
157
+ ```python
158
+ from passlib.context import CryptContext
159
+
160
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
161
+
162
+ def hash_password(password: str) -> str:
163
+ return pwd_context.hash(password)
164
+
165
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
166
+ return pwd_context.verify(plain_password, hashed_password)
167
+
168
+ async def authenticate_user(db: DbSession, email: str, password: str) -> User | None:
169
+ user = await db.scalar(select(User).where(User.email == email))
170
+ if not user:
171
+ return None
172
+ if not verify_password(password, user.hashed_password):
173
+ return None
174
+ return user
175
+ ```
176
+
177
+ ## CORS Configuration
178
+
179
+ ```python
180
+ from fastapi.middleware.cors import CORSMiddleware
181
+
182
+ app.add_middleware(
183
+ CORSMiddleware,
184
+ allow_origins=settings.cors_origins, # ["https://example.com"]
185
+ allow_credentials=True,
186
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
187
+ allow_headers=["*"],
188
+ expose_headers=["X-Request-ID"],
189
+ )
190
+ ```
191
+
192
+ ## Security Headers Middleware
193
+
194
+ ```python
195
+ from starlette.middleware.base import BaseHTTPMiddleware
196
+
197
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
198
+ async def dispatch(self, request: Request, call_next):
199
+ response = await call_next(request)
200
+ response.headers["X-Content-Type-Options"] = "nosniff"
201
+ response.headers["X-Frame-Options"] = "DENY"
202
+ response.headers["X-XSS-Protection"] = "1; mode=block"
203
+ response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
204
+ return response
205
+
206
+ app.add_middleware(SecurityHeadersMiddleware)
207
+ ```
208
+
209
+ ## Rate Limiting
210
+
211
+ ```python
212
+ from slowapi import Limiter
213
+ from slowapi.util import get_remote_address
214
+
215
+ limiter = Limiter(key_func=get_remote_address)
216
+ app.state.limiter = limiter
217
+
218
+ @router.get("/")
219
+ @limiter.limit("10/minute")
220
+ async def get_items(request: Request) -> list[Item]:
221
+ ...
222
+ ```
@@ -0,0 +1,251 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.py"
4
+ - "**/test_*.py"
5
+ ---
6
+
7
+ # FastAPI Testing Patterns
8
+
9
+ ## Test Client Setup
10
+
11
+ ```python
12
+ import pytest
13
+ from httpx import AsyncClient, ASGITransport
14
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
15
+ from sqlalchemy.orm import sessionmaker
16
+
17
+ from app.main import app
18
+ from app.database import Base, get_db
19
+
20
+ TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
21
+
22
+ @pytest.fixture(scope="session")
23
+ def event_loop():
24
+ import asyncio
25
+ loop = asyncio.new_event_loop()
26
+ yield loop
27
+ loop.close()
28
+
29
+ @pytest.fixture(scope="session")
30
+ async def engine():
31
+ engine = create_async_engine(TEST_DATABASE_URL)
32
+ async with engine.begin() as conn:
33
+ await conn.run_sync(Base.metadata.create_all)
34
+ yield engine
35
+ async with engine.begin() as conn:
36
+ await conn.run_sync(Base.metadata.drop_all)
37
+ await engine.dispose()
38
+
39
+ @pytest.fixture
40
+ async def db_session(engine):
41
+ async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
42
+ async with async_session() as session:
43
+ yield session
44
+ await session.rollback()
45
+
46
+ @pytest.fixture
47
+ async def client(db_session):
48
+ async def override_get_db():
49
+ yield db_session
50
+
51
+ app.dependency_overrides[get_db] = override_get_db
52
+
53
+ async with AsyncClient(
54
+ transport=ASGITransport(app=app),
55
+ base_url="http://test",
56
+ ) as client:
57
+ yield client
58
+
59
+ app.dependency_overrides.clear()
60
+ ```
61
+
62
+ ## API Tests
63
+
64
+ ```python
65
+ class TestUsersAPI:
66
+ async def test_create_user(self, client: AsyncClient):
67
+ response = await client.post("/api/v1/users", json={
68
+ "email": "test@example.com",
69
+ "password": "password123",
70
+ "name": "Test User",
71
+ })
72
+
73
+ assert response.status_code == 201
74
+ data = response.json()
75
+ assert data["email"] == "test@example.com"
76
+ assert "id" in data
77
+ assert "password" not in data
78
+
79
+ async def test_create_user_duplicate_email(self, client: AsyncClient, db_session):
80
+ # Create existing user
81
+ user = User(email="existing@example.com", name="Existing")
82
+ db_session.add(user)
83
+ await db_session.commit()
84
+
85
+ response = await client.post("/api/v1/users", json={
86
+ "email": "existing@example.com",
87
+ "password": "password123",
88
+ "name": "New User",
89
+ })
90
+
91
+ assert response.status_code == 409
92
+ assert "already exists" in response.json()["detail"]
93
+
94
+ async def test_get_user_not_found(self, client: AsyncClient):
95
+ response = await client.get("/api/v1/users/99999")
96
+
97
+ assert response.status_code == 404
98
+
99
+ async def test_list_users_pagination(self, client: AsyncClient, db_session):
100
+ # Create users
101
+ for i in range(25):
102
+ db_session.add(User(email=f"user{i}@example.com", name=f"User {i}"))
103
+ await db_session.commit()
104
+
105
+ response = await client.get("/api/v1/users?page=2&size=10")
106
+
107
+ assert response.status_code == 200
108
+ data = response.json()
109
+ assert len(data["items"]) == 10
110
+ assert data["total"] == 25
111
+ assert data["page"] == 2
112
+ ```
113
+
114
+ ## Authentication Tests
115
+
116
+ ```python
117
+ @pytest.fixture
118
+ async def auth_headers(client: AsyncClient, db_session):
119
+ # Create user
120
+ user = User(email="auth@example.com", name="Auth User")
121
+ user.set_password("password123")
122
+ db_session.add(user)
123
+ await db_session.commit()
124
+
125
+ # Get token
126
+ response = await client.post("/api/v1/auth/login", data={
127
+ "username": "auth@example.com",
128
+ "password": "password123",
129
+ })
130
+ token = response.json()["access_token"]
131
+
132
+ return {"Authorization": f"Bearer {token}"}
133
+
134
+ class TestAuthenticatedEndpoints:
135
+ async def test_get_me_unauthorized(self, client: AsyncClient):
136
+ response = await client.get("/api/v1/users/me")
137
+ assert response.status_code == 401
138
+
139
+ async def test_get_me_authorized(self, client: AsyncClient, auth_headers):
140
+ response = await client.get("/api/v1/users/me", headers=auth_headers)
141
+ assert response.status_code == 200
142
+ assert response.json()["email"] == "auth@example.com"
143
+ ```
144
+
145
+ ## Dependency Override
146
+
147
+ ```python
148
+ from unittest.mock import AsyncMock
149
+
150
+ @pytest.fixture
151
+ def mock_email_service():
152
+ return AsyncMock()
153
+
154
+ async def test_signup_sends_email(client: AsyncClient, mock_email_service):
155
+ app.dependency_overrides[get_email_service] = lambda: mock_email_service
156
+
157
+ response = await client.post("/api/v1/auth/signup", json={
158
+ "email": "new@example.com",
159
+ "password": "password123",
160
+ "name": "New User",
161
+ })
162
+
163
+ assert response.status_code == 201
164
+ mock_email_service.send_welcome.assert_called_once_with("new@example.com")
165
+
166
+ app.dependency_overrides.clear()
167
+ ```
168
+
169
+ ## WebSocket Tests
170
+
171
+ ```python
172
+ from httpx_ws import aconnect_ws
173
+
174
+ async def test_websocket_echo(client: AsyncClient):
175
+ async with aconnect_ws("http://test/ws", client) as ws:
176
+ await ws.send_text("Hello")
177
+ response = await ws.receive_text()
178
+ assert response == "Echo: Hello"
179
+
180
+ async def test_websocket_auth_required():
181
+ async with AsyncClient(
182
+ transport=ASGITransport(app=app),
183
+ base_url="http://test",
184
+ ) as client:
185
+ with pytest.raises(Exception):
186
+ async with aconnect_ws("http://test/ws", client) as ws:
187
+ pass # Should fail without token
188
+ ```
189
+
190
+ ## File Upload Tests
191
+
192
+ ```python
193
+ async def test_file_upload(client: AsyncClient, auth_headers):
194
+ files = {"file": ("test.txt", b"file content", "text/plain")}
195
+
196
+ response = await client.post(
197
+ "/api/v1/files/upload",
198
+ files=files,
199
+ headers=auth_headers,
200
+ )
201
+
202
+ assert response.status_code == 201
203
+ assert response.json()["filename"] == "test.txt"
204
+ ```
205
+
206
+ ## Parametrized Tests
207
+
208
+ ```python
209
+ @pytest.mark.parametrize("email,expected_valid", [
210
+ ("valid@example.com", True),
211
+ ("also.valid@example.co.uk", True),
212
+ ("invalid", False),
213
+ ("missing@", False),
214
+ ("@nodomain.com", False),
215
+ ])
216
+ async def test_email_validation(client: AsyncClient, email: str, expected_valid: bool):
217
+ response = await client.post("/api/v1/users", json={
218
+ "email": email,
219
+ "password": "password123",
220
+ "name": "Test",
221
+ })
222
+
223
+ if expected_valid:
224
+ assert response.status_code in (201, 409) # Created or duplicate
225
+ else:
226
+ assert response.status_code == 422
227
+ ```
228
+
229
+ ## Test Markers
230
+
231
+ ```python
232
+ # pyproject.toml
233
+ [tool.pytest.ini_options]
234
+ markers = [
235
+ "slow: marks tests as slow",
236
+ "integration: requires database",
237
+ ]
238
+
239
+ # Usage
240
+ @pytest.mark.slow
241
+ async def test_heavy_computation():
242
+ ...
243
+
244
+ @pytest.mark.integration
245
+ async def test_database_query():
246
+ ...
247
+
248
+ # Run specific markers
249
+ # pytest -m "not slow"
250
+ # pytest -m integration
251
+ ```