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:
Andrey Epifancev 2026-10-28 18:11:06 +04:00
parent 2a287480b6
commit 2dc9b4df03
2 changed files with 80 additions and 1 deletions

View File

@ -143,7 +143,23 @@ class UserUpdate(BaseModel):
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)
id: int

View 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"