From df943ea1c77b391d301caea52b8deed634519a77 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 30 Jul 2026 22:10:52 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein/team):=20\Z=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20$=20=D0=B2=20username-regex=20=E2=80=94=20?= =?UTF-8?q?422=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20500=20=D0=BD=D0=B0?= =?UTF-8?q?=20trailing=20newline=20(#2554)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 полем. --- tradein-mvp/backend/app/schemas/team.py | 9 ++++++++- tradein-mvp/backend/tests/test_team_api.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tradein-mvp/backend/app/schemas/team.py b/tradein-mvp/backend/app/schemas/team.py index a1022247..fe037389 100644 --- a/tradein-mvp/backend/app/schemas/team.py +++ b/tradein-mvp/backend/app/schemas/team.py @@ -16,7 +16,14 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator # (`app.core.rbac._propagate_authenticated_user` кодирует latin-1 с # 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): diff --git a/tradein-mvp/backend/tests/test_team_api.py b/tradein-mvp/backend/tests/test_team_api.py index 6a446885..2f1e40ce 100644 --- a/tradein-mvp/backend/tests/test_team_api.py +++ b/tradein-mvp/backend/tests/test_team_api.py @@ -519,6 +519,26 @@ def test_create_employee_non_ascii_username_422(client: TestClient, store: _Stor 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: store.add_user("mgr_a", hash_password("Secret123!"), role="manager") store.add_user("emp_dup", hash_password("Secret123!"), role="employee")