@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,267 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # Flask Security Patterns
7
+
8
+ ## Password Hashing
9
+
10
+ ```python
11
+ from werkzeug.security import generate_password_hash, check_password_hash
12
+
13
+ class User(db.Model):
14
+ id: Mapped[int] = mapped_column(primary_key=True)
15
+ email: Mapped[str] = mapped_column(unique=True)
16
+ password_hash: Mapped[str]
17
+
18
+ def set_password(self, password: str):
19
+ self.password_hash = generate_password_hash(password)
20
+
21
+ def check_password(self, password: str) -> bool:
22
+ return check_password_hash(self.password_hash, password)
23
+
24
+ # Usage
25
+ user = User(email="test@example.com")
26
+ user.set_password("secure_password")
27
+
28
+ if user.check_password("secure_password"):
29
+ print("Password correct")
30
+ ```
31
+
32
+ ## CSRF Protection
33
+
34
+ ```python
35
+ from flask_wtf.csrf import CSRFProtect
36
+
37
+ csrf = CSRFProtect()
38
+ csrf.init_app(app)
39
+
40
+ # In templates
41
+ <form method="post">
42
+ {{ csrf_token() }}
43
+ <!-- or -->
44
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
45
+ </form>
46
+
47
+ # For AJAX requests
48
+ <script>
49
+ fetch('/api/endpoint', {
50
+ method: 'POST',
51
+ headers: {
52
+ 'X-CSRFToken': '{{ csrf_token() }}'
53
+ },
54
+ body: JSON.stringify(data)
55
+ });
56
+ </script>
57
+
58
+ # Exempt API routes from CSRF
59
+ @csrf.exempt
60
+ @app.route("/api/webhook", methods=["POST"])
61
+ def webhook():
62
+ ...
63
+
64
+ # Or exempt entire blueprint
65
+ csrf.exempt(api_bp)
66
+ ```
67
+
68
+ ## Security Headers
69
+
70
+ ```python
71
+ from flask_talisman import Talisman
72
+
73
+ # Basic security headers
74
+ Talisman(app, force_https=True)
75
+
76
+ # Custom configuration
77
+ Talisman(
78
+ app,
79
+ force_https=True,
80
+ strict_transport_security=True,
81
+ strict_transport_security_max_age=31536000,
82
+ content_security_policy={
83
+ "default-src": "'self'",
84
+ "script-src": ["'self'", "cdn.example.com"],
85
+ "style-src": ["'self'", "'unsafe-inline'"],
86
+ "img-src": ["'self'", "data:", "*.example.com"],
87
+ },
88
+ )
89
+
90
+ # Manual headers
91
+ @app.after_request
92
+ def add_security_headers(response):
93
+ response.headers["X-Content-Type-Options"] = "nosniff"
94
+ response.headers["X-Frame-Options"] = "DENY"
95
+ response.headers["X-XSS-Protection"] = "1; mode=block"
96
+ return response
97
+ ```
98
+
99
+ ## Session Security
100
+
101
+ ```python
102
+ # Configuration
103
+ app.config.update(
104
+ SECRET_KEY=os.environ["SECRET_KEY"],
105
+ SESSION_COOKIE_SECURE=True, # HTTPS only
106
+ SESSION_COOKIE_HTTPONLY=True, # No JavaScript access
107
+ SESSION_COOKIE_SAMESITE="Lax", # CSRF protection
108
+ PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
109
+ )
110
+
111
+ # Server-side sessions with Redis
112
+ from flask_session import Session
113
+
114
+ app.config["SESSION_TYPE"] = "redis"
115
+ app.config["SESSION_REDIS"] = redis.from_url(os.environ["REDIS_URL"])
116
+ Session(app)
117
+ ```
118
+
119
+ ## Input Validation
120
+
121
+ ```python
122
+ from markupsafe import escape
123
+ from marshmallow import Schema, fields, validate
124
+
125
+ # Always validate input
126
+ class UserInputSchema(Schema):
127
+ name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
128
+ email = fields.Email(required=True)
129
+ bio = fields.Str(validate=validate.Length(max=500))
130
+
131
+ @users_bp.route("/", methods=["POST"])
132
+ def create_user():
133
+ schema = UserInputSchema()
134
+ data = schema.load(request.get_json()) # Validates and sanitizes
135
+ user = UserService.create(data)
136
+ return jsonify(UserSchema().dump(user)), 201
137
+
138
+ # Escape output for HTML
139
+ @app.route("/search")
140
+ def search():
141
+ query = request.args.get("q", "")
142
+ safe_query = escape(query) # Prevents XSS
143
+ return render_template("search.html", query=safe_query)
144
+ ```
145
+
146
+ ## SQL Injection Prevention
147
+
148
+ ```python
149
+ # GOOD - Use ORM or parameterized queries
150
+ user = User.query.filter_by(email=email).first()
151
+
152
+ # GOOD - Raw SQL with parameters
153
+ result = db.session.execute(
154
+ text("SELECT * FROM users WHERE email = :email"),
155
+ {"email": email}
156
+ )
157
+
158
+ # BAD - String interpolation (SQL injection vulnerable!)
159
+ result = db.session.execute(f"SELECT * FROM users WHERE email = '{email}'")
160
+ ```
161
+
162
+ ## API Key Authentication
163
+
164
+ ```python
165
+ from functools import wraps
166
+
167
+ def require_api_key(f):
168
+ @wraps(f)
169
+ def decorated(*args, **kwargs):
170
+ api_key = request.headers.get("X-API-Key")
171
+
172
+ if not api_key:
173
+ return jsonify({"error": "API key required"}), 401
174
+
175
+ # Constant-time comparison to prevent timing attacks
176
+ import hmac
177
+ valid_key = current_app.config["API_KEY"]
178
+ if not hmac.compare_digest(api_key, valid_key):
179
+ return jsonify({"error": "Invalid API key"}), 401
180
+
181
+ return f(*args, **kwargs)
182
+ return decorated
183
+
184
+ @api_bp.route("/data")
185
+ @require_api_key
186
+ def get_data():
187
+ return jsonify({"data": "sensitive"})
188
+ ```
189
+
190
+ ## Rate Limiting
191
+
192
+ ```python
193
+ from flask_limiter import Limiter
194
+ from flask_limiter.util import get_remote_address
195
+
196
+ limiter = Limiter(
197
+ key_func=get_remote_address,
198
+ default_limits=["200 per day", "50 per hour"],
199
+ storage_uri="redis://localhost:6379",
200
+ )
201
+
202
+ @auth_bp.route("/login", methods=["POST"])
203
+ @limiter.limit("5 per minute")
204
+ def login():
205
+ ...
206
+
207
+ # Per-user rate limiting
208
+ @limiter.limit("100 per hour", key_func=lambda: current_user.id)
209
+ def user_endpoint():
210
+ ...
211
+ ```
212
+
213
+ ## Secure File Uploads
214
+
215
+ ```python
216
+ from werkzeug.utils import secure_filename
217
+ import os
218
+
219
+ ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "pdf"}
220
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
221
+
222
+ app.config["MAX_CONTENT_LENGTH"] = MAX_CONTENT_LENGTH
223
+
224
+ def allowed_file(filename: str) -> bool:
225
+ return "." in filename and \
226
+ filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
227
+
228
+ @files_bp.route("/upload", methods=["POST"])
229
+ def upload_file():
230
+ if "file" not in request.files:
231
+ return jsonify({"error": "No file provided"}), 400
232
+
233
+ file = request.files["file"]
234
+
235
+ if file.filename == "":
236
+ return jsonify({"error": "No file selected"}), 400
237
+
238
+ if not allowed_file(file.filename):
239
+ return jsonify({"error": "File type not allowed"}), 400
240
+
241
+ # Secure the filename
242
+ filename = secure_filename(file.filename)
243
+
244
+ # Generate unique filename
245
+ unique_filename = f"{uuid.uuid4()}_{filename}"
246
+
247
+ # Save to secure location
248
+ file.save(os.path.join(app.config["UPLOAD_FOLDER"], unique_filename))
249
+
250
+ return jsonify({"filename": unique_filename}), 201
251
+ ```
252
+
253
+ ## Secrets Management
254
+
255
+ ```python
256
+ import os
257
+
258
+ # GOOD - Environment variables
259
+ app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
260
+ app.config["DATABASE_URL"] = os.environ["DATABASE_URL"]
261
+
262
+ # GOOD - Secrets file (not in repo)
263
+ # config/secrets.py (in .gitignore)
264
+
265
+ # BAD - Hardcoded secrets
266
+ app.config["SECRET_KEY"] = "hardcoded-secret" # NEVER do this
267
+ ```
@@ -0,0 +1,284 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.py"
4
+ - "**/test_*.py"
5
+ ---
6
+
7
+ # Flask Testing Patterns
8
+
9
+ ## Test Configuration
10
+
11
+ ```python
12
+ # conftest.py
13
+ import pytest
14
+ from app import create_app, db
15
+
16
+ @pytest.fixture(scope="session")
17
+ def app():
18
+ """Create application for testing."""
19
+ app = create_app("testing")
20
+ return app
21
+
22
+ @pytest.fixture
23
+ def client(app):
24
+ """Create test client."""
25
+ return app.test_client()
26
+
27
+ @pytest.fixture
28
+ def runner(app):
29
+ """Create CLI test runner."""
30
+ return app.test_cli_runner()
31
+
32
+ @pytest.fixture
33
+ def db_session(app):
34
+ """Create database session for testing."""
35
+ with app.app_context():
36
+ db.create_all()
37
+ yield db.session
38
+ db.session.rollback()
39
+ db.drop_all()
40
+ ```
41
+
42
+ ## Testing Config
43
+
44
+ ```python
45
+ # config.py
46
+ class TestingConfig:
47
+ TESTING = True
48
+ SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
49
+ WTF_CSRF_ENABLED = False
50
+ SECRET_KEY = "test-secret-key"
51
+ ```
52
+
53
+ ## API Endpoint Tests
54
+
55
+ ```python
56
+ class TestUsersAPI:
57
+ def test_create_user(self, client, db_session):
58
+ response = client.post("/api/v1/users", json={
59
+ "email": "test@example.com",
60
+ "password": "password123",
61
+ "name": "Test User",
62
+ })
63
+
64
+ assert response.status_code == 201
65
+ data = response.get_json()
66
+ assert data["email"] == "test@example.com"
67
+ assert "id" in data
68
+ assert "password" not in data
69
+
70
+ def test_create_user_duplicate_email(self, client, db_session):
71
+ # Create existing user
72
+ user = User(email="existing@example.com", name="Existing")
73
+ db_session.add(user)
74
+ db_session.commit()
75
+
76
+ response = client.post("/api/v1/users", json={
77
+ "email": "existing@example.com",
78
+ "password": "password123",
79
+ "name": "New User",
80
+ })
81
+
82
+ assert response.status_code == 409
83
+
84
+ def test_get_user_not_found(self, client):
85
+ response = client.get("/api/v1/users/99999")
86
+ assert response.status_code == 404
87
+
88
+ def test_list_users_pagination(self, client, db_session):
89
+ # Create users
90
+ for i in range(25):
91
+ db_session.add(User(email=f"user{i}@example.com", name=f"User {i}"))
92
+ db_session.commit()
93
+
94
+ response = client.get("/api/v1/users?page=2&size=10")
95
+
96
+ assert response.status_code == 200
97
+ data = response.get_json()
98
+ assert len(data["items"]) == 10
99
+ assert data["total"] == 25
100
+ ```
101
+
102
+ ## Authentication Tests
103
+
104
+ ```python
105
+ @pytest.fixture
106
+ def auth_headers(client, db_session):
107
+ """Create authenticated user and return headers."""
108
+ user = User(email="auth@example.com", name="Auth User")
109
+ user.set_password("password123")
110
+ db_session.add(user)
111
+ db_session.commit()
112
+
113
+ response = client.post("/api/v1/auth/login", json={
114
+ "email": "auth@example.com",
115
+ "password": "password123",
116
+ })
117
+ token = response.get_json()["access_token"]
118
+
119
+ return {"Authorization": f"Bearer {token}"}
120
+
121
+ class TestAuthenticatedEndpoints:
122
+ def test_get_me_unauthorized(self, client):
123
+ response = client.get("/api/v1/users/me")
124
+ assert response.status_code == 401
125
+
126
+ def test_get_me_authorized(self, client, auth_headers):
127
+ response = client.get("/api/v1/users/me", headers=auth_headers)
128
+ assert response.status_code == 200
129
+ assert response.get_json()["email"] == "auth@example.com"
130
+ ```
131
+
132
+ ## Form Tests
133
+
134
+ ```python
135
+ def test_login_form(client, db_session):
136
+ # Create user
137
+ user = User(email="test@example.com", name="Test")
138
+ user.set_password("password")
139
+ db_session.add(user)
140
+ db_session.commit()
141
+
142
+ response = client.post("/login", data={
143
+ "email": "test@example.com",
144
+ "password": "password",
145
+ }, follow_redirects=True)
146
+
147
+ assert response.status_code == 200
148
+ assert b"Dashboard" in response.data
149
+ ```
150
+
151
+ ## File Upload Tests
152
+
153
+ ```python
154
+ from io import BytesIO
155
+
156
+ def test_file_upload(client, auth_headers):
157
+ data = {
158
+ "file": (BytesIO(b"file content"), "test.txt"),
159
+ }
160
+
161
+ response = client.post(
162
+ "/api/v1/files/upload",
163
+ data=data,
164
+ content_type="multipart/form-data",
165
+ headers=auth_headers,
166
+ )
167
+
168
+ assert response.status_code == 201
169
+ assert response.get_json()["filename"] == "test.txt"
170
+ ```
171
+
172
+ ## CLI Command Tests
173
+
174
+ ```python
175
+ def test_init_db_command(runner):
176
+ result = runner.invoke(args=["init-db"])
177
+ assert result.exit_code == 0
178
+ assert "Database initialized" in result.output
179
+
180
+ def test_create_user_command(runner, db_session):
181
+ result = runner.invoke(args=[
182
+ "create-user",
183
+ "test@example.com",
184
+ "Test User",
185
+ "--admin",
186
+ ])
187
+
188
+ assert result.exit_code == 0
189
+ assert "Created user" in result.output
190
+
191
+ user = User.query.filter_by(email="test@example.com").first()
192
+ assert user is not None
193
+ assert user.is_admin is True
194
+ ```
195
+
196
+ ## Mocking External Services
197
+
198
+ ```python
199
+ from unittest.mock import patch, MagicMock
200
+
201
+ def test_send_email(client, db_session):
202
+ with patch("app.services.email.mail") as mock_mail:
203
+ response = client.post("/api/v1/users", json={
204
+ "email": "test@example.com",
205
+ "password": "password123",
206
+ "name": "Test User",
207
+ })
208
+
209
+ assert response.status_code == 201
210
+ mock_mail.send.assert_called_once()
211
+
212
+ def test_external_api_call(client):
213
+ with patch("app.services.external.requests") as mock_requests:
214
+ mock_requests.get.return_value = MagicMock(
215
+ status_code=200,
216
+ json=lambda: {"data": "mocked"},
217
+ )
218
+
219
+ response = client.get("/api/v1/external-data")
220
+
221
+ assert response.status_code == 200
222
+ assert response.get_json()["data"] == "mocked"
223
+ ```
224
+
225
+ ## Parametrized Tests
226
+
227
+ ```python
228
+ @pytest.mark.parametrize("email,expected_status", [
229
+ ("valid@example.com", 201),
230
+ ("also.valid@test.co.uk", 201),
231
+ ("invalid", 400),
232
+ ("missing@", 400),
233
+ ("@nodomain.com", 400),
234
+ ])
235
+ def test_email_validation(client, email: str, expected_status: int):
236
+ response = client.post("/api/v1/users", json={
237
+ "email": email,
238
+ "password": "password123",
239
+ "name": "Test",
240
+ })
241
+
242
+ assert response.status_code == expected_status
243
+ ```
244
+
245
+ ## Test Markers
246
+
247
+ ```python
248
+ # pytest.ini or pyproject.toml
249
+ [tool.pytest.ini_options]
250
+ markers = [
251
+ "slow: marks tests as slow",
252
+ "integration: requires database",
253
+ ]
254
+
255
+ # Usage
256
+ @pytest.mark.slow
257
+ def test_heavy_computation():
258
+ ...
259
+
260
+ @pytest.mark.integration
261
+ def test_database_query(db_session):
262
+ ...
263
+
264
+ # Run specific markers
265
+ # pytest -m "not slow"
266
+ # pytest -m integration
267
+ ```
268
+
269
+ ## Coverage Configuration
270
+
271
+ ```toml
272
+ # pyproject.toml
273
+ [tool.coverage.run]
274
+ source = ["app"]
275
+ omit = ["*/tests/*", "*/__init__.py"]
276
+
277
+ [tool.coverage.report]
278
+ exclude_lines = [
279
+ "pragma: no cover",
280
+ "if TYPE_CHECKING:",
281
+ "raise NotImplementedError",
282
+ ]
283
+ fail_under = 80
284
+ ```
@@ -0,0 +1,35 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python *)",
5
+ "Bash(python3 *)",
6
+ "Bash(flask *)",
7
+ "Bash(gunicorn *)",
8
+ "Bash(pytest *)",
9
+ "Bash(ruff *)",
10
+ "Bash(mypy *)",
11
+ "Bash(alembic *)",
12
+ "Bash(uv *)",
13
+ "Bash(poetry *)",
14
+ "Bash(pip *)",
15
+ "Bash(pip3 *)",
16
+ "Read",
17
+ "Edit",
18
+ "Write"
19
+ ],
20
+ "deny": [
21
+ "Bash(git push *)",
22
+ "Bash(git push)",
23
+ "Bash(rm -rf *)",
24
+ "Read(.env)",
25
+ "Read(.env.*)",
26
+ "Read(**/secrets/**)",
27
+ "Read(**/*.pem)"
28
+ ]
29
+ },
30
+ "env": {
31
+ "FLASK_DEBUG": "1",
32
+ "PYTHONDONTWRITEBYTECODE": "1",
33
+ "PYTHONUNBUFFERED": "1"
34
+ }
35
+ }