fix(users): выходные схемы пользователя без входных ограничений (/users/me 500 на .local)
UserInDB наследует новую UserReadBase (email/username/full_name без валидации формата). Вход (UserBase/UserCreate/UserUpdate) не менялся. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2a287480b6
commit
2dc9b4df03
@ -143,7 +143,23 @@ class UserUpdate(BaseModel):
|
|||||||
load_orgs: bool = Field(default=False)
|
load_orgs: bool = Field(default=False)
|
||||||
|
|
||||||
|
|
||||||
class UserInDB(UserBase):
|
class UserReadBase(BaseModel):
|
||||||
|
"""Выходная база пользователя без входных ограничений.
|
||||||
|
|
||||||
|
Запись в БД может не проходить входную валидацию (email на .local,
|
||||||
|
короткий username, '<' в full_name), но чтение не должно из-за этого
|
||||||
|
отвечать 500. Входные схемы (UserBase/UserCreate/UserUpdate) не меняются.
|
||||||
|
"""
|
||||||
|
|
||||||
|
email: str
|
||||||
|
username: str
|
||||||
|
full_name: Optional[str] = None
|
||||||
|
role_id: int = 3
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserInDB(UserReadBase):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
|
|||||||
63
api/tests/unit/test_user_schemas.py
Normal file
63
api/tests/unit/test_user_schemas.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from src.api.v1.deps import get_current_active_user
|
||||||
|
from src.api.v1.users import router as users_router
|
||||||
|
from src.db.session import get_db
|
||||||
|
from src.domain.schemas import UserAdminListResponse, UserCreate, UserInDB
|
||||||
|
|
||||||
|
_VALID = dict(
|
||||||
|
id=1,
|
||||||
|
email="admin@corp.example",
|
||||||
|
username="admin_1",
|
||||||
|
full_name="Иван Иванов",
|
||||||
|
role_id=1,
|
||||||
|
is_active=True,
|
||||||
|
created_at=datetime(2026, 1, 1),
|
||||||
|
updated_at=None,
|
||||||
|
org_units=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Записи, которые API на входе не пропустил бы, но которые могут лежать в БД
|
||||||
|
# (прямой INSERT, например perf-стенд): вывод не должен из-за них падать.
|
||||||
|
_LEGACY = [
|
||||||
|
{"email": "admin@corp.local"},
|
||||||
|
{"username": "rf"},
|
||||||
|
{"full_name": "a<b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("override", _LEGACY, ids=["email-local", "short-username", "lt-in-full-name"])
|
||||||
|
def test_output_schemas_accept_legacy_db_rows(override):
|
||||||
|
row = SimpleNamespace(**{**_VALID, **override})
|
||||||
|
assert UserAdminListResponse.model_validate(row).id == 1
|
||||||
|
assert UserInDB.model_validate(row).id == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_create_still_rejects_local_email():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
UserCreate(email="x@corp.local", username="user_1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_users_me_returns_200_for_local_email():
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(users_router, prefix="/api/v1")
|
||||||
|
row = SimpleNamespace(**{**_VALID, "email": "admin@corp.local", "username": "rf"})
|
||||||
|
|
||||||
|
async def _user():
|
||||||
|
return row
|
||||||
|
|
||||||
|
async def _db():
|
||||||
|
yield None
|
||||||
|
|
||||||
|
app.dependency_overrides[get_current_active_user] = _user
|
||||||
|
app.dependency_overrides[get_db] = _db
|
||||||
|
|
||||||
|
resp = TestClient(app).get("/api/v1/users/me")
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
assert resp.json()["email"] == "admin@corp.local"
|
||||||
Loading…
x
Reference in New Issue
Block a user