claude-code-templates 1.16.0 → 1.17.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/README.md +7 -7
- package/bin/create-claude-config.js +17 -8
- package/package.json +2 -3
- package/src/analytics/core/AgentAnalyzer.js +17 -3
- package/src/analytics/core/ProcessDetector.js +23 -7
- package/src/analytics/core/StateCalculator.js +102 -33
- package/src/analytics/data/DataCache.js +7 -7
- package/src/analytics-web/chats_mobile.html +2590 -0
- package/src/analytics-web/components/App.js +10 -10
- package/src/analytics-web/components/SessionTimer.js +1 -1
- package/src/analytics-web/components/Sidebar.js +5 -14
- package/src/analytics-web/index.html +932 -78
- package/src/analytics.js +263 -5
- package/src/chats-mobile.js +682 -0
- package/src/claude-api-proxy.js +460 -0
- package/src/file-operations.js +239 -36
- package/src/health-check.js +310 -0
- package/src/index.js +1252 -56
- package/src/tracking-service.js +31 -34
- package/components/agents/api-security-audit.md +0 -92
- package/components/agents/database-optimization.md +0 -94
- package/components/agents/react-performance-optimization.md +0 -64
- package/components/commands/check-file.md +0 -53
- package/components/commands/generate-tests.md +0 -68
- package/components/mcps/deepgraph-nextjs.json +0 -12
- package/components/mcps/deepgraph-react.json +0 -12
- package/components/mcps/deepgraph-typescript.json +0 -12
- package/components/mcps/deepgraph-vue.json +0 -12
- package/components/mcps/filesystem-access.json +0 -12
- package/components/mcps/github-integration.json +0 -11
- package/components/mcps/memory-integration.json +0 -8
- package/components/mcps/mysql-integration.json +0 -11
- package/components/mcps/postgresql-integration.json +0 -11
- package/components/mcps/web-fetch.json +0 -8
- package/src/analytics-web/components/AgentsPage.js +0 -4761
- package/templates/common/.claude/commands/git-workflow.md +0 -239
- package/templates/common/.claude/commands/project-setup.md +0 -316
- package/templates/common/.mcp.json +0 -41
- package/templates/common/CLAUDE.md +0 -109
- package/templates/common/README.md +0 -96
- package/templates/go/.mcp.json +0 -78
- package/templates/go/README.md +0 -25
- package/templates/javascript-typescript/.claude/commands/api-endpoint.md +0 -51
- package/templates/javascript-typescript/.claude/commands/debug.md +0 -52
- package/templates/javascript-typescript/.claude/commands/lint.md +0 -48
- package/templates/javascript-typescript/.claude/commands/npm-scripts.md +0 -48
- package/templates/javascript-typescript/.claude/commands/refactor.md +0 -55
- package/templates/javascript-typescript/.claude/commands/test.md +0 -61
- package/templates/javascript-typescript/.claude/commands/typescript-migrate.md +0 -51
- package/templates/javascript-typescript/.claude/settings.json +0 -142
- package/templates/javascript-typescript/.mcp.json +0 -80
- package/templates/javascript-typescript/CLAUDE.md +0 -185
- package/templates/javascript-typescript/README.md +0 -259
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/components.md +0 -63
- package/templates/javascript-typescript/examples/angular-app/.claude/commands/services.md +0 -62
- package/templates/javascript-typescript/examples/node-api/.claude/commands/api-endpoint.md +0 -46
- package/templates/javascript-typescript/examples/node-api/.claude/commands/database.md +0 -56
- package/templates/javascript-typescript/examples/node-api/.claude/commands/middleware.md +0 -61
- package/templates/javascript-typescript/examples/node-api/.claude/commands/route.md +0 -57
- package/templates/javascript-typescript/examples/node-api/CLAUDE.md +0 -102
- package/templates/javascript-typescript/examples/react-app/.claude/commands/component.md +0 -29
- package/templates/javascript-typescript/examples/react-app/.claude/commands/hooks.md +0 -44
- package/templates/javascript-typescript/examples/react-app/.claude/commands/state-management.md +0 -45
- package/templates/javascript-typescript/examples/react-app/CLAUDE.md +0 -81
- package/templates/javascript-typescript/examples/react-app/agents/react-performance-optimization.md +0 -530
- package/templates/javascript-typescript/examples/react-app/agents/react-state-management.md +0 -295
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/components.md +0 -46
- package/templates/javascript-typescript/examples/vue-app/.claude/commands/composables.md +0 -51
- package/templates/python/.claude/commands/lint.md +0 -111
- package/templates/python/.claude/commands/test.md +0 -73
- package/templates/python/.claude/settings.json +0 -153
- package/templates/python/.mcp.json +0 -78
- package/templates/python/CLAUDE.md +0 -276
- package/templates/python/examples/django-app/.claude/commands/admin.md +0 -264
- package/templates/python/examples/django-app/.claude/commands/django-model.md +0 -124
- package/templates/python/examples/django-app/.claude/commands/views.md +0 -222
- package/templates/python/examples/django-app/CLAUDE.md +0 -313
- package/templates/python/examples/django-app/agents/django-api-security.md +0 -642
- package/templates/python/examples/django-app/agents/django-database-optimization.md +0 -752
- package/templates/python/examples/fastapi-app/.claude/commands/api-endpoints.md +0 -513
- package/templates/python/examples/fastapi-app/.claude/commands/auth.md +0 -775
- package/templates/python/examples/fastapi-app/.claude/commands/database.md +0 -657
- package/templates/python/examples/fastapi-app/.claude/commands/deployment.md +0 -160
- package/templates/python/examples/fastapi-app/.claude/commands/testing.md +0 -927
- package/templates/python/examples/fastapi-app/CLAUDE.md +0 -229
- package/templates/python/examples/flask-app/.claude/commands/app-factory.md +0 -384
- package/templates/python/examples/flask-app/.claude/commands/blueprint.md +0 -243
- package/templates/python/examples/flask-app/.claude/commands/database.md +0 -410
- package/templates/python/examples/flask-app/.claude/commands/deployment.md +0 -620
- package/templates/python/examples/flask-app/.claude/commands/flask-route.md +0 -217
- package/templates/python/examples/flask-app/.claude/commands/testing.md +0 -559
- package/templates/python/examples/flask-app/CLAUDE.md +0 -391
- package/templates/ruby/.claude/commands/model.md +0 -360
- package/templates/ruby/.claude/commands/test.md +0 -480
- package/templates/ruby/.claude/settings.json +0 -146
- package/templates/ruby/.mcp.json +0 -83
- package/templates/ruby/CLAUDE.md +0 -284
- package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +0 -490
- package/templates/ruby/examples/rails-app/CLAUDE.md +0 -376
- package/templates/rust/.mcp.json +0 -78
- package/templates/rust/README.md +0 -26
|
@@ -1,927 +0,0 @@
|
|
|
1
|
-
# FastAPI Testing Framework
|
|
2
|
-
|
|
3
|
-
Comprehensive testing setup for FastAPI applications with pytest and async support.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Run all tests
|
|
9
|
-
pytest
|
|
10
|
-
|
|
11
|
-
# Run with coverage
|
|
12
|
-
pytest --cov=app --cov-report=html
|
|
13
|
-
|
|
14
|
-
# Run specific test file
|
|
15
|
-
pytest tests/test_api.py
|
|
16
|
-
|
|
17
|
-
# Run with verbose output
|
|
18
|
-
pytest -v -s
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Test Configuration
|
|
22
|
-
|
|
23
|
-
```python
|
|
24
|
-
# pytest.ini
|
|
25
|
-
[tool:pytest]
|
|
26
|
-
testpaths = tests
|
|
27
|
-
python_files = test_*.py
|
|
28
|
-
python_classes = Test*
|
|
29
|
-
python_functions = test_*
|
|
30
|
-
addopts =
|
|
31
|
-
--cov=app
|
|
32
|
-
--cov-report=term-missing
|
|
33
|
-
--cov-report=html:htmlcov
|
|
34
|
-
--asyncio-mode=auto
|
|
35
|
-
--strict-markers
|
|
36
|
-
--disable-warnings
|
|
37
|
-
markers =
|
|
38
|
-
unit: Unit tests
|
|
39
|
-
integration: Integration tests
|
|
40
|
-
e2e: End-to-end tests
|
|
41
|
-
slow: Slow running tests
|
|
42
|
-
auth: Authentication tests
|
|
43
|
-
api: API tests
|
|
44
|
-
asyncio_mode = auto
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Test Dependencies
|
|
48
|
-
|
|
49
|
-
```python
|
|
50
|
-
# requirements/test.txt
|
|
51
|
-
pytest>=7.0.0
|
|
52
|
-
pytest-asyncio>=0.21.0
|
|
53
|
-
pytest-cov>=4.0.0
|
|
54
|
-
httpx>=0.24.0
|
|
55
|
-
factory-boy>=3.2.0
|
|
56
|
-
faker>=18.0.0
|
|
57
|
-
respx>=0.20.0
|
|
58
|
-
pytest-mock>=3.10.0
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Test Fixtures
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
# tests/conftest.py
|
|
65
|
-
import pytest
|
|
66
|
-
import asyncio
|
|
67
|
-
from typing import AsyncGenerator, Generator
|
|
68
|
-
from fastapi.testclient import TestClient
|
|
69
|
-
from httpx import AsyncClient
|
|
70
|
-
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
71
|
-
from sqlalchemy.orm import sessionmaker
|
|
72
|
-
from app.main import app
|
|
73
|
-
from app.db.database import get_db, Base
|
|
74
|
-
from app.models.user import User
|
|
75
|
-
from app.core.security import get_password_hash
|
|
76
|
-
from tests.factories import UserFactory
|
|
77
|
-
|
|
78
|
-
# Test database URL
|
|
79
|
-
TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
|
|
80
|
-
|
|
81
|
-
@pytest.fixture(scope="session")
|
|
82
|
-
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
|
|
83
|
-
"""Create event loop for the test session."""
|
|
84
|
-
loop = asyncio.get_event_loop_policy().new_event_loop()
|
|
85
|
-
yield loop
|
|
86
|
-
loop.close()
|
|
87
|
-
|
|
88
|
-
@pytest.fixture(scope="session")
|
|
89
|
-
async def test_engine():
|
|
90
|
-
"""Create test database engine."""
|
|
91
|
-
engine = create_async_engine(
|
|
92
|
-
TEST_DATABASE_URL,
|
|
93
|
-
echo=False,
|
|
94
|
-
future=True
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
# Create tables
|
|
98
|
-
async with engine.begin() as conn:
|
|
99
|
-
await conn.run_sync(Base.metadata.create_all)
|
|
100
|
-
|
|
101
|
-
yield engine
|
|
102
|
-
|
|
103
|
-
# Drop tables and dispose engine
|
|
104
|
-
async with engine.begin() as conn:
|
|
105
|
-
await conn.run_sync(Base.metadata.drop_all)
|
|
106
|
-
|
|
107
|
-
await engine.dispose()
|
|
108
|
-
|
|
109
|
-
@pytest.fixture
|
|
110
|
-
async def db_session(test_engine) -> AsyncGenerator[AsyncSession, None]:
|
|
111
|
-
"""Create database session for testing."""
|
|
112
|
-
TestSessionLocal = sessionmaker(
|
|
113
|
-
test_engine,
|
|
114
|
-
class_=AsyncSession,
|
|
115
|
-
expire_on_commit=False
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
async with TestSessionLocal() as session:
|
|
119
|
-
yield session
|
|
120
|
-
|
|
121
|
-
@pytest.fixture
|
|
122
|
-
def override_get_db(db_session: AsyncSession) -> Generator:
|
|
123
|
-
"""Override database dependency."""
|
|
124
|
-
async def _override_get_db():
|
|
125
|
-
yield db_session
|
|
126
|
-
|
|
127
|
-
app.dependency_overrides[get_db] = _override_get_db
|
|
128
|
-
yield
|
|
129
|
-
app.dependency_overrides = {}
|
|
130
|
-
|
|
131
|
-
@pytest.fixture
|
|
132
|
-
def client(override_get_db) -> Generator[TestClient, None, None]:
|
|
133
|
-
"""Create test client."""
|
|
134
|
-
with TestClient(app) as test_client:
|
|
135
|
-
yield test_client
|
|
136
|
-
|
|
137
|
-
@pytest.fixture
|
|
138
|
-
async def async_client(override_get_db) -> AsyncGenerator[AsyncClient, None]:
|
|
139
|
-
"""Create async test client."""
|
|
140
|
-
async with AsyncClient(app=app, base_url="http://test") as ac:
|
|
141
|
-
yield ac
|
|
142
|
-
|
|
143
|
-
@pytest.fixture
|
|
144
|
-
async def test_user(db_session: AsyncSession) -> User:
|
|
145
|
-
"""Create test user."""
|
|
146
|
-
user_data = {
|
|
147
|
-
"username": "testuser",
|
|
148
|
-
"email": "test@example.com",
|
|
149
|
-
"hashed_password": get_password_hash("testpass123"),
|
|
150
|
-
"first_name": "Test",
|
|
151
|
-
"last_name": "User",
|
|
152
|
-
"is_active": True,
|
|
153
|
-
"is_superuser": False
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
user = User(**user_data)
|
|
157
|
-
db_session.add(user)
|
|
158
|
-
await db_session.commit()
|
|
159
|
-
await db_session.refresh(user)
|
|
160
|
-
return user
|
|
161
|
-
|
|
162
|
-
@pytest.fixture
|
|
163
|
-
async def superuser(db_session: AsyncSession) -> User:
|
|
164
|
-
"""Create superuser."""
|
|
165
|
-
user_data = {
|
|
166
|
-
"username": "admin",
|
|
167
|
-
"email": "admin@example.com",
|
|
168
|
-
"hashed_password": get_password_hash("adminpass123"),
|
|
169
|
-
"first_name": "Admin",
|
|
170
|
-
"last_name": "User",
|
|
171
|
-
"is_active": True,
|
|
172
|
-
"is_superuser": True
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
user = User(**user_data)
|
|
176
|
-
db_session.add(user)
|
|
177
|
-
await db_session.commit()
|
|
178
|
-
await db_session.refresh(user)
|
|
179
|
-
return user
|
|
180
|
-
|
|
181
|
-
@pytest.fixture
|
|
182
|
-
def user_token(test_user: User) -> str:
|
|
183
|
-
"""Create authentication token for test user."""
|
|
184
|
-
from app.core.security import create_access_token
|
|
185
|
-
return create_access_token(subject=test_user.id)
|
|
186
|
-
|
|
187
|
-
@pytest.fixture
|
|
188
|
-
def superuser_token(superuser: User) -> str:
|
|
189
|
-
"""Create authentication token for superuser."""
|
|
190
|
-
from app.core.security import create_access_token
|
|
191
|
-
return create_access_token(subject=superuser.id)
|
|
192
|
-
|
|
193
|
-
@pytest.fixture
|
|
194
|
-
def auth_headers(user_token: str) -> dict[str, str]:
|
|
195
|
-
"""Create authorization headers."""
|
|
196
|
-
return {"Authorization": f"Bearer {user_token}"}
|
|
197
|
-
|
|
198
|
-
@pytest.fixture
|
|
199
|
-
def superuser_headers(superuser_token: str) -> dict[str, str]:
|
|
200
|
-
"""Create superuser authorization headers."""
|
|
201
|
-
return {"Authorization": f"Bearer {superuser_token}"}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## Test Factories
|
|
205
|
-
|
|
206
|
-
```python
|
|
207
|
-
# tests/factories.py
|
|
208
|
-
import factory
|
|
209
|
-
from factory import Faker, SubFactory
|
|
210
|
-
from app.models.user import User
|
|
211
|
-
from app.models.post import Post
|
|
212
|
-
from app.core.security import get_password_hash
|
|
213
|
-
|
|
214
|
-
class UserFactory(factory.Factory):
|
|
215
|
-
"""Factory for User model."""
|
|
216
|
-
|
|
217
|
-
class Meta:
|
|
218
|
-
model = User
|
|
219
|
-
|
|
220
|
-
username = Faker('user_name')
|
|
221
|
-
email = Faker('email')
|
|
222
|
-
first_name = Faker('first_name')
|
|
223
|
-
last_name = Faker('last_name')
|
|
224
|
-
hashed_password = factory.LazyAttribute(lambda obj: get_password_hash('testpass123'))
|
|
225
|
-
is_active = True
|
|
226
|
-
is_superuser = False
|
|
227
|
-
bio = Faker('text', max_nb_chars=200)
|
|
228
|
-
|
|
229
|
-
class SuperUserFactory(UserFactory):
|
|
230
|
-
"""Factory for superuser."""
|
|
231
|
-
username = 'admin'
|
|
232
|
-
email = 'admin@example.com'
|
|
233
|
-
first_name = 'Admin'
|
|
234
|
-
last_name = 'User'
|
|
235
|
-
is_superuser = True
|
|
236
|
-
|
|
237
|
-
class PostFactory(factory.Factory):
|
|
238
|
-
"""Factory for Post model."""
|
|
239
|
-
|
|
240
|
-
class Meta:
|
|
241
|
-
model = Post
|
|
242
|
-
|
|
243
|
-
title = Faker('sentence', nb_words=4)
|
|
244
|
-
content = Faker('text', max_nb_chars=1000)
|
|
245
|
-
slug = Faker('slug')
|
|
246
|
-
is_published = True
|
|
247
|
-
author = SubFactory(UserFactory)
|
|
248
|
-
|
|
249
|
-
class InactiveUserFactory(UserFactory):
|
|
250
|
-
"""Factory for inactive user."""
|
|
251
|
-
is_active = False
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## API Testing
|
|
255
|
-
|
|
256
|
-
```python
|
|
257
|
-
# tests/test_api/test_users.py
|
|
258
|
-
import pytest
|
|
259
|
-
from httpx import AsyncClient
|
|
260
|
-
from app.models.user import User
|
|
261
|
-
from tests.factories import UserFactory
|
|
262
|
-
|
|
263
|
-
class TestUserAPI:
|
|
264
|
-
"""Test User API endpoints."""
|
|
265
|
-
|
|
266
|
-
@pytest.mark.asyncio
|
|
267
|
-
async def test_create_user(
|
|
268
|
-
self,
|
|
269
|
-
async_client: AsyncClient,
|
|
270
|
-
superuser_headers: dict
|
|
271
|
-
):
|
|
272
|
-
"""Test POST /api/v1/users/."""
|
|
273
|
-
user_data = {
|
|
274
|
-
"username": "newuser",
|
|
275
|
-
"email": "new@example.com",
|
|
276
|
-
"password": "newpass123",
|
|
277
|
-
"first_name": "New",
|
|
278
|
-
"last_name": "User"
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
response = await async_client.post(
|
|
282
|
-
"/api/v1/users/",
|
|
283
|
-
json=user_data,
|
|
284
|
-
headers=superuser_headers
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
assert response.status_code == 201
|
|
288
|
-
data = response.json()
|
|
289
|
-
assert data["username"] == user_data["username"]
|
|
290
|
-
assert data["email"] == user_data["email"]
|
|
291
|
-
assert "password" not in data
|
|
292
|
-
assert "hashed_password" not in data
|
|
293
|
-
|
|
294
|
-
@pytest.mark.asyncio
|
|
295
|
-
async def test_get_users(
|
|
296
|
-
self,
|
|
297
|
-
async_client: AsyncClient,
|
|
298
|
-
test_user: User,
|
|
299
|
-
auth_headers: dict
|
|
300
|
-
):
|
|
301
|
-
"""Test GET /api/v1/users/."""
|
|
302
|
-
response = await async_client.get(
|
|
303
|
-
"/api/v1/users/",
|
|
304
|
-
headers=auth_headers
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
assert response.status_code == 200
|
|
308
|
-
data = response.json()
|
|
309
|
-
assert "items" in data
|
|
310
|
-
assert "total" in data
|
|
311
|
-
assert "page" in data
|
|
312
|
-
assert "size" in data
|
|
313
|
-
assert len(data["items"]) >= 1
|
|
314
|
-
|
|
315
|
-
@pytest.mark.asyncio
|
|
316
|
-
async def test_get_user(
|
|
317
|
-
self,
|
|
318
|
-
async_client: AsyncClient,
|
|
319
|
-
test_user: User,
|
|
320
|
-
auth_headers: dict
|
|
321
|
-
):
|
|
322
|
-
"""Test GET /api/v1/users/{id}."""
|
|
323
|
-
response = await async_client.get(
|
|
324
|
-
f"/api/v1/users/{test_user.id}",
|
|
325
|
-
headers=auth_headers
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
assert response.status_code == 200
|
|
329
|
-
data = response.json()
|
|
330
|
-
assert data["id"] == test_user.id
|
|
331
|
-
assert data["username"] == test_user.username
|
|
332
|
-
assert data["email"] == test_user.email
|
|
333
|
-
|
|
334
|
-
@pytest.mark.asyncio
|
|
335
|
-
async def test_update_user(
|
|
336
|
-
self,
|
|
337
|
-
async_client: AsyncClient,
|
|
338
|
-
test_user: User,
|
|
339
|
-
auth_headers: dict
|
|
340
|
-
):
|
|
341
|
-
"""Test PUT /api/v1/users/{id}."""
|
|
342
|
-
update_data = {
|
|
343
|
-
"first_name": "Updated",
|
|
344
|
-
"last_name": "Name",
|
|
345
|
-
"bio": "Updated bio"
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
response = await async_client.put(
|
|
349
|
-
f"/api/v1/users/{test_user.id}",
|
|
350
|
-
json=update_data,
|
|
351
|
-
headers=auth_headers
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
assert response.status_code == 200
|
|
355
|
-
data = response.json()
|
|
356
|
-
assert data["first_name"] == update_data["first_name"]
|
|
357
|
-
assert data["last_name"] == update_data["last_name"]
|
|
358
|
-
assert data["bio"] == update_data["bio"]
|
|
359
|
-
|
|
360
|
-
@pytest.mark.asyncio
|
|
361
|
-
async def test_delete_user(
|
|
362
|
-
self,
|
|
363
|
-
async_client: AsyncClient,
|
|
364
|
-
test_user: User,
|
|
365
|
-
superuser_headers: dict
|
|
366
|
-
):
|
|
367
|
-
"""Test DELETE /api/v1/users/{id}."""
|
|
368
|
-
response = await async_client.delete(
|
|
369
|
-
f"/api/v1/users/{test_user.id}",
|
|
370
|
-
headers=superuser_headers
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
assert response.status_code == 204
|
|
374
|
-
|
|
375
|
-
# Verify user is deleted
|
|
376
|
-
get_response = await async_client.get(
|
|
377
|
-
f"/api/v1/users/{test_user.id}",
|
|
378
|
-
headers=superuser_headers
|
|
379
|
-
)
|
|
380
|
-
assert get_response.status_code == 404
|
|
381
|
-
|
|
382
|
-
@pytest.mark.asyncio
|
|
383
|
-
async def test_unauthorized_access(self, async_client: AsyncClient):
|
|
384
|
-
"""Test unauthorized access to protected endpoints."""
|
|
385
|
-
response = await async_client.get("/api/v1/users/")
|
|
386
|
-
assert response.status_code == 401
|
|
387
|
-
|
|
388
|
-
@pytest.mark.asyncio
|
|
389
|
-
async def test_forbidden_access(
|
|
390
|
-
self,
|
|
391
|
-
async_client: AsyncClient,
|
|
392
|
-
auth_headers: dict
|
|
393
|
-
):
|
|
394
|
-
"""Test forbidden access to admin endpoints."""
|
|
395
|
-
user_data = {
|
|
396
|
-
"username": "unauthorized",
|
|
397
|
-
"email": "unauthorized@example.com",
|
|
398
|
-
"password": "pass123"
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
response = await async_client.post(
|
|
402
|
-
"/api/v1/users/",
|
|
403
|
-
json=user_data,
|
|
404
|
-
headers=auth_headers # Regular user, not superuser
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
assert response.status_code == 403
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
## Authentication Testing
|
|
411
|
-
|
|
412
|
-
```python
|
|
413
|
-
# tests/test_api/test_auth.py
|
|
414
|
-
import pytest
|
|
415
|
-
from httpx import AsyncClient
|
|
416
|
-
from app.models.user import User
|
|
417
|
-
from app.core.security import create_access_token, decode_token
|
|
418
|
-
|
|
419
|
-
class TestAuthAPI:
|
|
420
|
-
"""Test authentication API endpoints."""
|
|
421
|
-
|
|
422
|
-
@pytest.mark.asyncio
|
|
423
|
-
async def test_register(
|
|
424
|
-
self,
|
|
425
|
-
async_client: AsyncClient
|
|
426
|
-
):
|
|
427
|
-
"""Test user registration."""
|
|
428
|
-
user_data = {
|
|
429
|
-
"username": "newuser",
|
|
430
|
-
"email": "new@example.com",
|
|
431
|
-
"password": "newpass123",
|
|
432
|
-
"first_name": "New",
|
|
433
|
-
"last_name": "User"
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
response = await async_client.post(
|
|
437
|
-
"/api/v1/auth/register",
|
|
438
|
-
json=user_data
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
assert response.status_code == 201
|
|
442
|
-
data = response.json()
|
|
443
|
-
assert data["username"] == user_data["username"]
|
|
444
|
-
assert data["email"] == user_data["email"]
|
|
445
|
-
assert "password" not in data
|
|
446
|
-
|
|
447
|
-
@pytest.mark.asyncio
|
|
448
|
-
async def test_register_duplicate_email(
|
|
449
|
-
self,
|
|
450
|
-
async_client: AsyncClient,
|
|
451
|
-
test_user: User
|
|
452
|
-
):
|
|
453
|
-
"""Test registration with duplicate email."""
|
|
454
|
-
user_data = {
|
|
455
|
-
"username": "different",
|
|
456
|
-
"email": test_user.email, # Duplicate email
|
|
457
|
-
"password": "pass123",
|
|
458
|
-
"first_name": "Test",
|
|
459
|
-
"last_name": "User"
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
response = await async_client.post(
|
|
463
|
-
"/api/v1/auth/register",
|
|
464
|
-
json=user_data
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
assert response.status_code == 400
|
|
468
|
-
assert "Email already registered" in response.json()["detail"]
|
|
469
|
-
|
|
470
|
-
@pytest.mark.asyncio
|
|
471
|
-
async def test_login(
|
|
472
|
-
self,
|
|
473
|
-
async_client: AsyncClient,
|
|
474
|
-
test_user: User
|
|
475
|
-
):
|
|
476
|
-
"""Test user login."""
|
|
477
|
-
login_data = {
|
|
478
|
-
"username": test_user.username,
|
|
479
|
-
"password": "testpass123"
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
response = await async_client.post(
|
|
483
|
-
"/api/v1/auth/login",
|
|
484
|
-
data=login_data,
|
|
485
|
-
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
assert response.status_code == 200
|
|
489
|
-
data = response.json()
|
|
490
|
-
assert "access_token" in data
|
|
491
|
-
assert "refresh_token" in data
|
|
492
|
-
assert data["token_type"] == "bearer"
|
|
493
|
-
assert "expires_in" in data
|
|
494
|
-
|
|
495
|
-
# Verify token is valid
|
|
496
|
-
payload = decode_token(data["access_token"])
|
|
497
|
-
assert payload is not None
|
|
498
|
-
assert payload["sub"] == str(test_user.id)
|
|
499
|
-
|
|
500
|
-
@pytest.mark.asyncio
|
|
501
|
-
async def test_login_invalid_credentials(
|
|
502
|
-
self,
|
|
503
|
-
async_client: AsyncClient,
|
|
504
|
-
test_user: User
|
|
505
|
-
):
|
|
506
|
-
"""Test login with invalid credentials."""
|
|
507
|
-
login_data = {
|
|
508
|
-
"username": test_user.username,
|
|
509
|
-
"password": "wrongpassword"
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
response = await async_client.post(
|
|
513
|
-
"/api/v1/auth/login",
|
|
514
|
-
data=login_data,
|
|
515
|
-
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
assert response.status_code == 401
|
|
519
|
-
assert "Incorrect username or password" in response.json()["detail"]
|
|
520
|
-
|
|
521
|
-
@pytest.mark.asyncio
|
|
522
|
-
async def test_get_current_user(
|
|
523
|
-
self,
|
|
524
|
-
async_client: AsyncClient,
|
|
525
|
-
test_user: User,
|
|
526
|
-
auth_headers: dict
|
|
527
|
-
):
|
|
528
|
-
"""Test get current user endpoint."""
|
|
529
|
-
response = await async_client.get(
|
|
530
|
-
"/api/v1/auth/me",
|
|
531
|
-
headers=auth_headers
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
assert response.status_code == 200
|
|
535
|
-
data = response.json()
|
|
536
|
-
assert data["id"] == test_user.id
|
|
537
|
-
assert data["username"] == test_user.username
|
|
538
|
-
assert data["email"] == test_user.email
|
|
539
|
-
|
|
540
|
-
@pytest.mark.asyncio
|
|
541
|
-
async def test_refresh_token(
|
|
542
|
-
self,
|
|
543
|
-
async_client: AsyncClient,
|
|
544
|
-
test_user: User
|
|
545
|
-
):
|
|
546
|
-
"""Test token refresh."""
|
|
547
|
-
from app.core.security import create_refresh_token
|
|
548
|
-
|
|
549
|
-
refresh_token = create_refresh_token(subject=test_user.id)
|
|
550
|
-
|
|
551
|
-
response = await async_client.post(
|
|
552
|
-
"/api/v1/auth/refresh",
|
|
553
|
-
json={"refresh_token": refresh_token}
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
assert response.status_code == 200
|
|
557
|
-
data = response.json()
|
|
558
|
-
assert "access_token" in data
|
|
559
|
-
assert "refresh_token" in data
|
|
560
|
-
assert data["token_type"] == "bearer"
|
|
561
|
-
|
|
562
|
-
@pytest.mark.asyncio
|
|
563
|
-
async def test_change_password(
|
|
564
|
-
self,
|
|
565
|
-
async_client: AsyncClient,
|
|
566
|
-
test_user: User,
|
|
567
|
-
auth_headers: dict
|
|
568
|
-
):
|
|
569
|
-
"""Test password change."""
|
|
570
|
-
password_data = {
|
|
571
|
-
"current_password": "testpass123",
|
|
572
|
-
"new_password": "newpass123"
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
response = await async_client.post(
|
|
576
|
-
"/api/v1/auth/change-password",
|
|
577
|
-
json=password_data,
|
|
578
|
-
headers=auth_headers
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
assert response.status_code == 200
|
|
582
|
-
assert "Password changed successfully" in response.json()["message"]
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
## Model Testing
|
|
586
|
-
|
|
587
|
-
```python
|
|
588
|
-
# tests/test_models/test_user.py
|
|
589
|
-
import pytest
|
|
590
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
591
|
-
from app.models.user import User
|
|
592
|
-
from app.core.security import verify_password, get_password_hash
|
|
593
|
-
|
|
594
|
-
class TestUserModel:
|
|
595
|
-
"""Test User model."""
|
|
596
|
-
|
|
597
|
-
@pytest.mark.asyncio
|
|
598
|
-
async def test_create_user(self, db_session: AsyncSession):
|
|
599
|
-
"""Test user creation."""
|
|
600
|
-
user_data = {
|
|
601
|
-
"username": "testuser",
|
|
602
|
-
"email": "test@example.com",
|
|
603
|
-
"hashed_password": get_password_hash("password123"),
|
|
604
|
-
"first_name": "Test",
|
|
605
|
-
"last_name": "User"
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
user = User(**user_data)
|
|
609
|
-
db_session.add(user)
|
|
610
|
-
await db_session.commit()
|
|
611
|
-
await db_session.refresh(user)
|
|
612
|
-
|
|
613
|
-
assert user.id is not None
|
|
614
|
-
assert user.username == "testuser"
|
|
615
|
-
assert user.email == "test@example.com"
|
|
616
|
-
assert user.full_name == "Test User"
|
|
617
|
-
assert user.is_active is True
|
|
618
|
-
assert user.is_superuser is False
|
|
619
|
-
assert user.created_at is not None
|
|
620
|
-
assert user.updated_at is not None
|
|
621
|
-
|
|
622
|
-
def test_password_verification(self):
|
|
623
|
-
"""Test password verification."""
|
|
624
|
-
password = "testpassword123"
|
|
625
|
-
hashed = get_password_hash(password)
|
|
626
|
-
|
|
627
|
-
user = User(
|
|
628
|
-
username="test",
|
|
629
|
-
email="test@example.com",
|
|
630
|
-
hashed_password=hashed,
|
|
631
|
-
first_name="Test",
|
|
632
|
-
last_name="User"
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
assert user.verify_password(password)
|
|
636
|
-
assert not user.verify_password("wrongpassword")
|
|
637
|
-
|
|
638
|
-
def test_user_dict_excludes_password(self):
|
|
639
|
-
"""Test that dict() method excludes password."""
|
|
640
|
-
user = User(
|
|
641
|
-
username="test",
|
|
642
|
-
email="test@example.com",
|
|
643
|
-
hashed_password="hashed_password",
|
|
644
|
-
first_name="Test",
|
|
645
|
-
last_name="User"
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
user_dict = user.dict()
|
|
649
|
-
assert "hashed_password" not in user_dict
|
|
650
|
-
assert "username" in user_dict
|
|
651
|
-
assert "email" in user_dict
|
|
652
|
-
|
|
653
|
-
def test_user_repr(self):
|
|
654
|
-
"""Test user string representation."""
|
|
655
|
-
user = User(
|
|
656
|
-
id=1,
|
|
657
|
-
username="test",
|
|
658
|
-
email="test@example.com",
|
|
659
|
-
hashed_password="hash",
|
|
660
|
-
first_name="Test",
|
|
661
|
-
last_name="User"
|
|
662
|
-
)
|
|
663
|
-
|
|
664
|
-
assert repr(user) == "<User(id=1)>"
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
## Repository Testing
|
|
668
|
-
|
|
669
|
-
```python
|
|
670
|
-
# tests/test_repositories/test_user.py
|
|
671
|
-
import pytest
|
|
672
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
673
|
-
from app.models.user import User
|
|
674
|
-
from app.repositories.user import UserRepository
|
|
675
|
-
from app.core.security import get_password_hash
|
|
676
|
-
|
|
677
|
-
class TestUserRepository:
|
|
678
|
-
"""Test UserRepository."""
|
|
679
|
-
|
|
680
|
-
@pytest.mark.asyncio
|
|
681
|
-
async def test_create_user(self, db_session: AsyncSession):
|
|
682
|
-
"""Test user creation through repository."""
|
|
683
|
-
repo = UserRepository(User, db_session)
|
|
684
|
-
|
|
685
|
-
user_data = {
|
|
686
|
-
"username": "repouser",
|
|
687
|
-
"email": "repo@example.com",
|
|
688
|
-
"hashed_password": get_password_hash("password123"),
|
|
689
|
-
"first_name": "Repo",
|
|
690
|
-
"last_name": "User"
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
user = await repo.create(user_data)
|
|
694
|
-
|
|
695
|
-
assert user.id is not None
|
|
696
|
-
assert user.username == "repouser"
|
|
697
|
-
assert user.email == "repo@example.com"
|
|
698
|
-
|
|
699
|
-
@pytest.mark.asyncio
|
|
700
|
-
async def test_get_by_email(self, db_session: AsyncSession, test_user: User):
|
|
701
|
-
"""Test get user by email."""
|
|
702
|
-
repo = UserRepository(User, db_session)
|
|
703
|
-
|
|
704
|
-
found_user = await repo.get_by_email(test_user.email)
|
|
705
|
-
|
|
706
|
-
assert found_user is not None
|
|
707
|
-
assert found_user.id == test_user.id
|
|
708
|
-
assert found_user.email == test_user.email
|
|
709
|
-
|
|
710
|
-
@pytest.mark.asyncio
|
|
711
|
-
async def test_get_by_username(self, db_session: AsyncSession, test_user: User):
|
|
712
|
-
"""Test get user by username."""
|
|
713
|
-
repo = UserRepository(User, db_session)
|
|
714
|
-
|
|
715
|
-
found_user = await repo.get_by_username(test_user.username)
|
|
716
|
-
|
|
717
|
-
assert found_user is not None
|
|
718
|
-
assert found_user.id == test_user.id
|
|
719
|
-
assert found_user.username == test_user.username
|
|
720
|
-
|
|
721
|
-
@pytest.mark.asyncio
|
|
722
|
-
async def test_get_multi_with_pagination(
|
|
723
|
-
self,
|
|
724
|
-
db_session: AsyncSession,
|
|
725
|
-
test_user: User
|
|
726
|
-
):
|
|
727
|
-
"""Test get multiple users with pagination."""
|
|
728
|
-
repo = UserRepository(User, db_session)
|
|
729
|
-
|
|
730
|
-
# Create additional users
|
|
731
|
-
for i in range(5):
|
|
732
|
-
user_data = {
|
|
733
|
-
"username": f"user{i}",
|
|
734
|
-
"email": f"user{i}@example.com",
|
|
735
|
-
"hashed_password": get_password_hash("password123"),
|
|
736
|
-
"first_name": f"User{i}",
|
|
737
|
-
"last_name": "Test"
|
|
738
|
-
}
|
|
739
|
-
await repo.create(user_data)
|
|
740
|
-
|
|
741
|
-
# Test pagination
|
|
742
|
-
users = await repo.get_multi(skip=0, limit=3)
|
|
743
|
-
assert len(users) == 3
|
|
744
|
-
|
|
745
|
-
users_page_2 = await repo.get_multi(skip=3, limit=3)
|
|
746
|
-
assert len(users_page_2) >= 1 # At least test_user
|
|
747
|
-
|
|
748
|
-
@pytest.mark.asyncio
|
|
749
|
-
async def test_update_user(self, db_session: AsyncSession, test_user: User):
|
|
750
|
-
"""Test user update."""
|
|
751
|
-
repo = UserRepository(User, db_session)
|
|
752
|
-
|
|
753
|
-
updated_user = await repo.update(test_user.id, {
|
|
754
|
-
"first_name": "Updated",
|
|
755
|
-
"bio": "Updated bio"
|
|
756
|
-
})
|
|
757
|
-
|
|
758
|
-
assert updated_user is not None
|
|
759
|
-
assert updated_user.first_name == "Updated"
|
|
760
|
-
assert updated_user.bio == "Updated bio"
|
|
761
|
-
|
|
762
|
-
@pytest.mark.asyncio
|
|
763
|
-
async def test_delete_user(self, db_session: AsyncSession, test_user: User):
|
|
764
|
-
"""Test user deletion."""
|
|
765
|
-
repo = UserRepository(User, db_session)
|
|
766
|
-
|
|
767
|
-
result = await repo.delete(test_user.id)
|
|
768
|
-
assert result is True
|
|
769
|
-
|
|
770
|
-
# Verify user is deleted
|
|
771
|
-
deleted_user = await repo.get(test_user.id)
|
|
772
|
-
assert deleted_user is None
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
## Performance Testing
|
|
776
|
-
|
|
777
|
-
```python
|
|
778
|
-
# tests/test_performance.py
|
|
779
|
-
import pytest
|
|
780
|
-
import time
|
|
781
|
-
import asyncio
|
|
782
|
-
from httpx import AsyncClient
|
|
783
|
-
from app.models.user import User
|
|
784
|
-
from tests.factories import UserFactory
|
|
785
|
-
|
|
786
|
-
@pytest.mark.slow
|
|
787
|
-
class TestPerformance:
|
|
788
|
-
"""Test application performance."""
|
|
789
|
-
|
|
790
|
-
@pytest.mark.asyncio
|
|
791
|
-
async def test_concurrent_requests(
|
|
792
|
-
self,
|
|
793
|
-
async_client: AsyncClient,
|
|
794
|
-
auth_headers: dict
|
|
795
|
-
):
|
|
796
|
-
"""Test concurrent API requests."""
|
|
797
|
-
|
|
798
|
-
async def make_request():
|
|
799
|
-
response = await async_client.get(
|
|
800
|
-
"/api/v1/users/",
|
|
801
|
-
headers=auth_headers
|
|
802
|
-
)
|
|
803
|
-
return response.status_code
|
|
804
|
-
|
|
805
|
-
# Make 10 concurrent requests
|
|
806
|
-
start_time = time.time()
|
|
807
|
-
tasks = [make_request() for _ in range(10)]
|
|
808
|
-
results = await asyncio.gather(*tasks)
|
|
809
|
-
end_time = time.time()
|
|
810
|
-
|
|
811
|
-
# All requests should succeed
|
|
812
|
-
assert all(status == 200 for status in results)
|
|
813
|
-
|
|
814
|
-
# Should complete within reasonable time
|
|
815
|
-
assert (end_time - start_time) < 5.0
|
|
816
|
-
|
|
817
|
-
@pytest.mark.asyncio
|
|
818
|
-
async def test_large_dataset_pagination(
|
|
819
|
-
self,
|
|
820
|
-
async_client: AsyncClient,
|
|
821
|
-
db_session,
|
|
822
|
-
auth_headers: dict
|
|
823
|
-
):
|
|
824
|
-
"""Test pagination with large dataset."""
|
|
825
|
-
# Create 100 users
|
|
826
|
-
users = []
|
|
827
|
-
for i in range(100):
|
|
828
|
-
user = UserFactory.build()
|
|
829
|
-
users.append(user)
|
|
830
|
-
|
|
831
|
-
db_session.add_all(users)
|
|
832
|
-
await db_session.commit()
|
|
833
|
-
|
|
834
|
-
# Test pagination performance
|
|
835
|
-
start_time = time.time()
|
|
836
|
-
response = await async_client.get(
|
|
837
|
-
"/api/v1/users/?skip=0&limit=50",
|
|
838
|
-
headers=auth_headers
|
|
839
|
-
)
|
|
840
|
-
end_time = time.time()
|
|
841
|
-
|
|
842
|
-
assert response.status_code == 200
|
|
843
|
-
data = response.json()
|
|
844
|
-
assert len(data["items"]) == 50
|
|
845
|
-
|
|
846
|
-
# Should complete quickly
|
|
847
|
-
assert (end_time - start_time) < 1.0
|
|
848
|
-
```
|
|
849
|
-
|
|
850
|
-
## Mocking External Services
|
|
851
|
-
|
|
852
|
-
```python
|
|
853
|
-
# tests/test_external.py
|
|
854
|
-
import pytest
|
|
855
|
-
import respx
|
|
856
|
-
import httpx
|
|
857
|
-
from app.services.email import EmailService
|
|
858
|
-
|
|
859
|
-
class TestExternalServices:
|
|
860
|
-
"""Test external service integrations."""
|
|
861
|
-
|
|
862
|
-
@pytest.mark.asyncio
|
|
863
|
-
@respx.mock
|
|
864
|
-
async def test_email_service(
|
|
865
|
-
self,
|
|
866
|
-
async_client: AsyncClient
|
|
867
|
-
):
|
|
868
|
-
"""Test email service with mocked external API."""
|
|
869
|
-
# Mock email service API
|
|
870
|
-
respx.post("https://api.emailservice.com/send").mock(
|
|
871
|
-
return_value=httpx.Response(
|
|
872
|
-
200,
|
|
873
|
-
json={"message": "Email sent successfully"}
|
|
874
|
-
)
|
|
875
|
-
)
|
|
876
|
-
|
|
877
|
-
email_service = EmailService()
|
|
878
|
-
result = await email_service.send_email(
|
|
879
|
-
to="test@example.com",
|
|
880
|
-
subject="Test",
|
|
881
|
-
body="Test email"
|
|
882
|
-
)
|
|
883
|
-
|
|
884
|
-
assert result["success"] is True
|
|
885
|
-
```
|
|
886
|
-
|
|
887
|
-
## Test Utilities
|
|
888
|
-
|
|
889
|
-
```python
|
|
890
|
-
# tests/utils.py
|
|
891
|
-
from typing import Dict, Any
|
|
892
|
-
from httpx import Response
|
|
893
|
-
import json
|
|
894
|
-
|
|
895
|
-
def assert_response_status(response: Response, expected_status: int = 200):
|
|
896
|
-
"""Assert response status code."""
|
|
897
|
-
assert response.status_code == expected_status, f"Expected {expected_status}, got {response.status_code}. Response: {response.text}"
|
|
898
|
-
|
|
899
|
-
def assert_response_json(response: Response, expected_keys: list[str] = None):
|
|
900
|
-
"""Assert response is valid JSON with expected keys."""
|
|
901
|
-
assert response.headers.get("content-type") == "application/json"
|
|
902
|
-
data = response.json()
|
|
903
|
-
|
|
904
|
-
if expected_keys:
|
|
905
|
-
for key in expected_keys:
|
|
906
|
-
assert key in data, f"Missing key '{key}' in response"
|
|
907
|
-
|
|
908
|
-
return data
|
|
909
|
-
|
|
910
|
-
def create_auth_headers(token: str) -> Dict[str, str]:
|
|
911
|
-
"""Create authorization headers with token."""
|
|
912
|
-
return {"Authorization": f"Bearer {token}"}
|
|
913
|
-
|
|
914
|
-
async def create_test_users(db_session, count: int = 5) -> list:
|
|
915
|
-
"""Create multiple test users."""
|
|
916
|
-
from tests.factories import UserFactory
|
|
917
|
-
|
|
918
|
-
users = []
|
|
919
|
-
for i in range(count):
|
|
920
|
-
user = UserFactory.build()
|
|
921
|
-
users.append(user)
|
|
922
|
-
|
|
923
|
-
db_session.add_all(users)
|
|
924
|
-
await db_session.commit()
|
|
925
|
-
|
|
926
|
-
return users
|
|
927
|
-
```
|