Files
ljwx-children/tests/unit/test_api_users.py
T
brunoandClaude Sonnet 4.5 9b7a0f6ec2 test: add comprehensive risk assessments API test suite (Issue #37)
Created complete test coverage for risk assessment endpoints:
- 25 test cases covering all 3 endpoints
- POST /risk-assessments (11 tests)
- GET /risk-assessments (5 tests)
- GET /risk-assessments/{id} (5 tests)
- Edge cases and error handling (4 tests)

Test features:
✅ Access control (guardian, psychologist, administrator)
✅ Ollama integration (mocked with AsyncMock)
✅ Fallback mechanisms
✅ Pagination and filtering
✅ Error handling and edge cases
✅ Data validation

Dependencies and fixes:
- Updated httpx (0.26.0 → 0.27.2) for pytest-httpx compatibility
- Fixed JWT imports (import jwt → from jose import jwt)
- Fixed Pydantic v2 field_validator syntax
- Fixed model import conflicts (removed duplicate User model)
- Updated backend/models/__init__.py to import left_behind_children models
- Added missing auth exports (blacklist_token, is_token_blacklisted, can_access_risk_assessment)

Known issue (⚠️ BLOCKING):
- SQLite UUID type incompatibility preventing tests from running
- Error: SQLiteTypeCompiler can't render UUID type
- Need to implement UUID-to-String adapter for SQLite in tests
- All 25 tests fail at setup phase

Files created/modified:
- tests/unit/test_api_risk_assessments.py (NEW - 700+ lines)
- requirements.txt (updated httpx, commented safety)
- backend/auth/__init__.py (added exports)
- backend/auth/utils.py (fixed JWT import)
- backend/auth/dependencies.py (fixed JWT import)
- backend/api/data.py (fixed Pydantic validator)
- backend/models/__init__.py (switched to left_behind_children models)

Next steps:
1. Fix UUID/SQLite compatibility (use String(36) or UUID adapter)
2. Run tests successfully
3. Achieve >95% coverage target

Progress: Issue #37 - 70% complete (tests written, need execution)

Related: #38, #39, #40, #41, #42

🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-01-01 14:23:55 +08:00

393 lines
12 KiB
Python

"""
Unit tests for backend.api.users module.
Tests:
- GET /users/me - Get current user profile
- PATCH /users/me - Update current user profile
- GET /users/{user_id} - Get user by ID (with RBAC)
- GET /users - List users (admin only)
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from backend.main import app
from backend.models import Base, get_session
from backend.models.left_behind_children import User
from backend.auth.utils import hash_password
# ============================================================================
# TEST DATABASE SETUP
# ============================================================================
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def override_get_session():
"""Override database session for testing."""
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_session] = override_get_session
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup_database():
"""Create tables before each test and drop after."""
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
def create_and_login_user(phone: str, password: str, name: str, role: str, age: int = None):
"""Helper: Create user and return access token."""
# Register
registration_data = {
"phone": phone,
"password": password,
"name": name,
"role": role
}
if age is not None:
registration_data["age"] = age
if age < 14:
registration_data["parental_consent_guardian_phone"] = "+8613800000000"
registration_data["parental_consent_signature"] = "consent-sig"
client.post("/auth/register", json=registration_data)
# Login
login_response = client.post("/auth/login", json={
"phone": phone,
"password": password
})
return login_response.json()["access_token"]
# ============================================================================
# TEST CASES
# ============================================================================
class TestGetCurrentUserProfile:
"""Test GET /users/me endpoint."""
def test_get_current_user_success(self):
"""Should return current user's profile."""
token = create_and_login_user(
phone="+8613800138000",
password="Password123!",
name="小明",
role="child",
age=15
)
response = client.get("/users/me", headers={
"Authorization": f"Bearer {token}"
})
assert response.status_code == 200
data = response.json()
assert data["phone"] == "+8613800138000"
assert data["name"] == "小明"
assert data["role"] == "child"
assert data["age"] == 15
assert data["is_active"] is True
def test_get_current_user_unauthorized(self):
"""Should reject request without token."""
response = client.get("/users/me")
assert response.status_code == 403 # Missing Authorization header
def test_get_current_user_invalid_token(self):
"""Should reject request with invalid token."""
response = client.get("/users/me", headers={
"Authorization": "Bearer invalid.token"
})
assert response.status_code == 401
class TestUpdateCurrentUserProfile:
"""Test PATCH /users/me endpoint."""
def test_update_name_success(self):
"""Should update user's name successfully."""
token = create_and_login_user(
phone="+8613800138000",
password="Password123!",
name="Original Name",
role="guardian"
)
response = client.patch("/users/me", headers={
"Authorization": f"Bearer {token}"
}, json={
"name": "Updated Name"
})
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Name"
def test_update_age_for_child(self):
"""Should allow child to update age."""
token = create_and_login_user(
phone="+8613800138000",
password="Password123!",
name="小明",
role="child",
age=12
)
response = client.patch("/users/me", headers={
"Authorization": f"Bearer {token}"
}, json={
"age": 13 # Birthday!
})
assert response.status_code == 200
data = response.json()
assert data["age"] == 13
def test_update_age_for_non_child_rejected(self):
"""Should reject age update for non-child users."""
token = create_and_login_user(
phone="+8613800138000",
password="Password123!",
name="Guardian",
role="guardian"
)
response = client.patch("/users/me", headers={
"Authorization": f"Bearer {token}"
}, json={
"age": 45 # Should be rejected
})
assert response.status_code == 400
assert "only be updated for users with role 'child'" in response.json()["detail"]
class TestGetUserById:
"""Test GET /users/{user_id} endpoint."""
def test_get_own_profile(self):
"""Should allow user to access own profile."""
token = create_and_login_user(
phone="+8613800138000",
password="Password123!",
name="Test User",
role="teacher"
)
# Get user ID from /users/me
me_response = client.get("/users/me", headers={
"Authorization": f"Bearer {token}"
})
user_id = me_response.json()["id"]
# Get profile by ID
response = client.get(f"/users/{user_id}", headers={
"Authorization": f"Bearer {token}"
})
assert response.status_code == 200
data = response.json()
assert data["id"] == user_id
def test_administrator_can_access_any_profile(self):
"""Should allow administrator to access any user's profile."""
# Create admin
admin_token = create_and_login_user(
phone="+8613800138001",
password="AdminPass123!",
name="Administrator",
role="administrator"
)
# Create regular user
user_token = create_and_login_user(
phone="+8613800138002",
password="UserPass123!",
name="Regular User",
role="parent"
)
# Get user ID
user_response = client.get("/users/me", headers={
"Authorization": f"Bearer {user_token}"
})
user_id = user_response.json()["id"]
# Admin accesses user's profile
response = client.get(f"/users/{user_id}", headers={
"Authorization": f"Bearer {admin_token}"
})
assert response.status_code == 200
def test_access_denied_for_unrelated_user(self):
"""Should deny access to unrelated user's profile."""
# Create two unrelated users
token1 = create_and_login_user(
phone="+8613800138001",
password="Pass123!",
name="User 1",
role="teacher"
)
token2 = create_and_login_user(
phone="+8613800138002",
password="Pass123!",
name="User 2",
role="psychologist"
)
# Get User 2's ID
user2_response = client.get("/users/me", headers={
"Authorization": f"Bearer {token2}"
})
user2_id = user2_response.json()["id"]
# User 1 tries to access User 2's profile
response = client.get(f"/users/{user2_id}", headers={
"Authorization": f"Bearer {token1}"
})
assert response.status_code == 403
def test_user_not_found(self):
"""Should return 404 for non-existent user."""
token = create_and_login_user(
phone="+8613800138000",
password="Pass123!",
name="Test User",
role="administrator"
)
response = client.get("/users/non-existent-id", headers={
"Authorization": f"Bearer {token}"
})
assert response.status_code == 404
class TestListUsers:
"""Test GET /users endpoint."""
def test_list_users_admin_only(self):
"""Should only allow administrators to list users."""
# Create non-admin user
non_admin_token = create_and_login_user(
phone="+8613800138000",
password="Pass123!",
name="Non-Admin",
role="teacher"
)
response = client.get("/users", headers={
"Authorization": f"Bearer {non_admin_token}"
})
assert response.status_code == 403
def test_list_users_success(self):
"""Should list users for administrator."""
# Create multiple users
create_and_login_user("+8613800138001", "Pass1!", "User 1", "child", 10)
create_and_login_user("+8613800138002", "Pass2!", "User 2", "guardian")
create_and_login_user("+8613800138003", "Pass3!", "User 3", "parent")
# Create admin
admin_token = create_and_login_user(
phone="+8613800138000",
password="AdminPass123!",
name="Admin",
role="administrator"
)
response = client.get("/users", headers={
"Authorization": f"Bearer {admin_token}"
})
assert response.status_code == 200
data = response.json()
assert "users" in data
assert "total" in data
assert "page" in data
assert data["total"] == 4 # 3 users + 1 admin
def test_list_users_pagination(self):
"""Should paginate user list."""
# Create admin
admin_token = create_and_login_user(
phone="+8613800138000",
password="AdminPass123!",
name="Admin",
role="administrator"
)
# Create 5 more users
for i in range(1, 6):
create_and_login_user(
phone=f"+861380013800{i}",
password="Pass123!",
name=f"User {i}",
role="teacher"
)
# Get page 1 with page_size=3
response = client.get("/users?page=1&page_size=3", headers={
"Authorization": f"Bearer {admin_token}"
})
assert response.status_code == 200
data = response.json()
assert len(data["users"]) == 3
assert data["total"] == 6 # 5 users + 1 admin
assert data["page"] == 1
assert data["total_pages"] == 2 # 6 users / 3 per page
def test_list_users_filter_by_role(self):
"""Should filter users by role."""
# Create users with different roles
create_and_login_user("+8613800138001", "Pass1!", "Child 1", "child", 12)
create_and_login_user("+8613800138002", "Pass2!", "Child 2", "child", 14)
create_and_login_user("+8613800138003", "Pass3!", "Guardian", "guardian")
# Create admin
admin_token = create_and_login_user(
phone="+8613800138000",
password="AdminPass123!",
name="Admin",
role="administrator"
)
# Filter by role=child
response = client.get("/users?role=child", headers={
"Authorization": f"Bearer {admin_token}"
})
assert response.status_code == 200
data = response.json()
assert data["total"] == 2 # Only 2 children
assert all(user["role"] == "child" for user in data["users"])