fix(tradein/team): \Z вместо $ в username-regex — 422 вместо 500 на trailing newline (#2554)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 13s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m18s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 13s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m18s
Deep-review seed'а (#2564) нашёл смежный дефект в уже смерженном коде (#2563): Python `$` матчит перед trailing newline (re.match(r'...\$', 'admin\n') -> True), а Postgres `~` в CHECK tradein_users_username_ascii_ck (миграция 193) - False. username="admin\n" проходил Pydantic-валидацию и падал уже в БД -> 500 вместо честного 422. `\Z` - конец строки без поблажки на trailing newline, совпадает с семантикой Postgres `~`. Grep по app/schemas/ (pattern=/regex=/re.compile/re.match/re.fullmatch) - других regex-валидаторов с `$` в схемах trade-in нет, team.py - единственный файл с regex-based полем.
This commit is contained in:
parent
2e05a16a14
commit
df943ea1c7
2 changed files with 28 additions and 1 deletions
|
|
@ -16,7 +16,14 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
# (`app.core.rbac._propagate_authenticated_user` кодирует latin-1 с
|
# (`app.core.rbac._propagate_authenticated_user` кодирует latin-1 с
|
||||||
# errors="replace"), поэтому валидация формы обязательна на границе API,
|
# errors="replace"), поэтому валидация формы обязательна на границе API,
|
||||||
# а не только на уровне БД.
|
# а не только на уровне БД.
|
||||||
_USERNAME_RE = re.compile(r"^[A-Za-z0-9._-]{3,64}$")
|
#
|
||||||
|
# `\Z`, НЕ `$` — deep-review seed #2564: в Python `$` матчит перед trailing
|
||||||
|
# newline (`re.match(r'...\$', 'admin\n')` → True), а Postgres `~` в CHECK
|
||||||
|
# tradein_users_username_ascii_ck (миграция 193) — False. С `$` строка
|
||||||
|
# "admin\n" проходила бы Pydantic-валидацию и падала уже в БД → 500 вместо
|
||||||
|
# честного 422. `\Z` — конец строки БЕЗ поблажки на trailing newline, совпадает
|
||||||
|
# с семантикой Postgres `~`.
|
||||||
|
_USERNAME_RE = re.compile(r"^[A-Za-z0-9._-]{3,64}\Z")
|
||||||
|
|
||||||
|
|
||||||
class QuotaStatusOut(BaseModel):
|
class QuotaStatusOut(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,26 @@ def test_create_employee_non_ascii_username_422(client: TestClient, store: _Stor
|
||||||
assert resp.status_code == 422
|
assert resp.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("username", ["admin\n", "user1\n"])
|
||||||
|
def test_create_employee_trailing_newline_username_422_not_500(
|
||||||
|
client: TestClient, store: _Store, username: str
|
||||||
|
) -> None:
|
||||||
|
"""Deep-review seed #2564: Python `$` matches BEFORE a trailing newline
|
||||||
|
(`re.match(r'...\\$', 'admin\\n')` → True), but Postgres `~` (CHECK
|
||||||
|
tradein_users_username_ascii_ck, migration 193) does NOT — a username with a
|
||||||
|
trailing "\\n" used to pass Pydantic validation and crash in the DB (500)
|
||||||
|
instead of a clean 422. `_USERNAME_RE` now uses `\\Z`, matching Postgres `~`
|
||||||
|
semantics exactly."""
|
||||||
|
store.add_user("mgr_a", hash_password("Secret123!"), role="manager")
|
||||||
|
_login(client, "mgr_a", "Secret123!")
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
"/api/v1/team/employees",
|
||||||
|
json={"username": username, "password": "Secret123!"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 422, resp.text
|
||||||
|
|
||||||
|
|
||||||
def test_create_employee_duplicate_username_409(client: TestClient, store: _Store) -> None:
|
def test_create_employee_duplicate_username_409(client: TestClient, store: _Store) -> None:
|
||||||
store.add_user("mgr_a", hash_password("Secret123!"), role="manager")
|
store.add_user("mgr_a", hash_password("Secret123!"), role="manager")
|
||||||
store.add_user("emp_dup", hash_password("Secret123!"), role="employee")
|
store.add_user("emp_dup", hash_password("Secret123!"), role="employee")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue