kin: BATON-BIZ-004 Удалить дублирующую настройку логирования в telegram.py
This commit is contained in:
parent
6444b30d17
commit
baf05b6d84
2 changed files with 447 additions and 16 deletions
|
|
@ -168,25 +168,17 @@ async def test_signal_message_contains_utc_marker():
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Criterion 3 — SignalAggregator preserved with '# v2.0 feature' marker (static)
|
# Criterion 3 — SignalAggregator removed (BATON-BIZ-004: dead code cleanup)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_signal_aggregator_class_preserved_in_telegram():
|
def test_signal_aggregator_class_removed_from_telegram():
|
||||||
"""SignalAggregator class must still exist in telegram.py (ADR-004, reserved for v2)."""
|
"""SignalAggregator must be removed from telegram.py (BATON-BIZ-004)."""
|
||||||
source = (_BACKEND_DIR / "telegram.py").read_text()
|
source = (_BACKEND_DIR / "telegram.py").read_text()
|
||||||
assert "class SignalAggregator" in source
|
assert "class SignalAggregator" not in source
|
||||||
|
|
||||||
|
|
||||||
def test_signal_aggregator_has_v2_feature_comment():
|
def test_signal_aggregator_not_referenced_in_telegram():
|
||||||
"""The line immediately before 'class SignalAggregator' must contain '# v2.0 feature'."""
|
"""telegram.py must not reference SignalAggregator at all (BATON-BIZ-004)."""
|
||||||
lines = (_BACKEND_DIR / "telegram.py").read_text().splitlines()
|
source = (_BACKEND_DIR / "telegram.py").read_text()
|
||||||
class_line_idx = next(
|
assert "SignalAggregator" not in source
|
||||||
(i for i, line in enumerate(lines) if "class SignalAggregator" in line), None
|
|
||||||
)
|
|
||||||
assert class_line_idx is not None, "class SignalAggregator not found in telegram.py"
|
|
||||||
assert class_line_idx > 0, "SignalAggregator is on the first line — no preceding comment line"
|
|
||||||
preceding_line = lines[class_line_idx - 1]
|
|
||||||
assert "# v2.0 feature" in preceding_line, (
|
|
||||||
f"Expected '# v2.0 feature' on line before class SignalAggregator, got: {preceding_line!r}"
|
|
||||||
)
|
|
||||||
|
|
|
||||||
439
tests/test_baton_008_frontend.py
Normal file
439
tests/test_baton_008_frontend.py
Normal file
|
|
@ -0,0 +1,439 @@
|
||||||
|
"""
|
||||||
|
Tests for BATON-008: Frontend registration module.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
1. index.html — форма регистрации с полями email, login, password присутствует
|
||||||
|
2. index.html — НЕТ захардкоженных VAPID-ключей в HTML-атрибутах (decision #1333)
|
||||||
|
3. app.js — вызов /api/push/public-key (не старый /api/vapid-public-key) (decision #1331)
|
||||||
|
4. app.js — guard для PushManager (decision #1332)
|
||||||
|
5. app.js — обработчик для кнопки регистрации (#btn-register → _handleSignUp)
|
||||||
|
6. app.js — переключение между view-login и view-register
|
||||||
|
7. app.js — показ ошибок пользователю (_setRegStatus)
|
||||||
|
8. GET /api/push/public-key → 200 с vapid_public_key (API контракт)
|
||||||
|
9. POST /api/auth/register с валидными данными → 201 (API контракт)
|
||||||
|
10. POST /api/auth/register с дублирующим email → 409
|
||||||
|
11. POST /api/auth/register с дублирующим login → 409
|
||||||
|
12. POST /api/auth/register с невалидным email → 422
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
INDEX_HTML = PROJECT_ROOT / "frontend" / "index.html"
|
||||||
|
APP_JS = PROJECT_ROOT / "frontend" / "app.js"
|
||||||
|
|
||||||
|
from tests.conftest import make_app_client
|
||||||
|
|
||||||
|
_VALID_PAYLOAD = {
|
||||||
|
"email": "frontend_test@example.com",
|
||||||
|
"login": "frontenduser",
|
||||||
|
"password": "strongpassword123",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HTML static analysis — Criterion 1: поля формы регистрации
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_email_field() -> None:
|
||||||
|
"""index.html должен содержать поле email для регистрации (id=reg-email)."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="reg-email"' in content, (
|
||||||
|
"index.html не содержит поле с id='reg-email'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_login_field() -> None:
|
||||||
|
"""index.html должен содержать поле логина для регистрации (id=reg-login)."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="reg-login"' in content, (
|
||||||
|
"index.html не содержит поле с id='reg-login'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_password_field() -> None:
|
||||||
|
"""index.html должен содержать поле пароля для регистрации (id=reg-password)."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="reg-password"' in content, (
|
||||||
|
"index.html не содержит поле с id='reg-password'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_email_field_has_correct_type() -> None:
|
||||||
|
"""Поле email регистрации должно иметь type='email'."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
# Ищем input с id=reg-email и type=email в любом порядке атрибутов
|
||||||
|
email_input_block = re.search(
|
||||||
|
r'<input[^>]*id="reg-email"[^>]*>', content, re.DOTALL
|
||||||
|
)
|
||||||
|
assert email_input_block is not None, "Не найден input с id='reg-email'"
|
||||||
|
assert 'type="email"' in email_input_block.group(0), (
|
||||||
|
"Поле reg-email не имеет type='email'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_password_field_has_correct_type() -> None:
|
||||||
|
"""Поле пароля регистрации должно иметь type='password'."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
password_input_block = re.search(
|
||||||
|
r'<input[^>]*id="reg-password"[^>]*>', content, re.DOTALL
|
||||||
|
)
|
||||||
|
assert password_input_block is not None, "Не найден input с id='reg-password'"
|
||||||
|
assert 'type="password"' in password_input_block.group(0), (
|
||||||
|
"Поле reg-password не имеет type='password'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_register_button() -> None:
|
||||||
|
"""index.html должен содержать кнопку регистрации (id=btn-register)."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="btn-register"' in content, (
|
||||||
|
"index.html не содержит кнопку с id='btn-register'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_switch_to_register_button() -> None:
|
||||||
|
"""index.html должен содержать кнопку переключения на форму регистрации (id=btn-switch-to-register)."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="btn-switch-to-register"' in content, (
|
||||||
|
"index.html не содержит кнопку с id='btn-switch-to-register'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_view_register_div() -> None:
|
||||||
|
"""index.html должен содержать блок view-register для формы регистрации."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="view-register"' in content, (
|
||||||
|
"index.html не содержит блок с id='view-register'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_view_login_div() -> None:
|
||||||
|
"""index.html должен содержать блок view-login для онбординга."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="view-login"' in content, (
|
||||||
|
"index.html не содержит блок с id='view-login'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_has_reg_status_element() -> None:
|
||||||
|
"""index.html должен содержать элемент статуса регистрации (id=reg-status)."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert 'id="reg-status"' in content, (
|
||||||
|
"index.html не содержит элемент с id='reg-status'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HTML static analysis — Criterion 2: НЕТ захардкоженного VAPID в HTML (decision #1333)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_no_hardcoded_vapid_key_in_meta() -> None:
|
||||||
|
"""index.html НЕ должен содержать VAPID-ключ захардкоженным в meta-теге (decision #1333)."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
# VAPID public key — это URL-safe base64 строка длиной 87 символов (без padding)
|
||||||
|
# Ищем характерный паттерн в meta-атрибутах
|
||||||
|
vapid_in_meta = re.search(
|
||||||
|
r'<meta[^>]+content\s*=\s*["\'][A-Za-z0-9_\-]{60,}["\'][^>]*>',
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
assert vapid_in_meta is None, (
|
||||||
|
f"Найден meta-тег с длинной строкой (возможный VAPID-ключ): "
|
||||||
|
f"{vapid_in_meta.group(0) if vapid_in_meta else ''}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_no_vapid_key_attribute_pattern() -> None:
|
||||||
|
"""index.html НЕ должен содержать data-vapid-key или аналогичные атрибуты."""
|
||||||
|
content = INDEX_HTML.read_text(encoding="utf-8")
|
||||||
|
assert "vapid" not in content.lower(), (
|
||||||
|
"index.html содержит упоминание 'vapid' — VAPID ключ должен читаться через API, "
|
||||||
|
"а не быть захардкожен в HTML (decision #1333)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# app.js static analysis — Criterion 3: /api/push/public-key endpoint (decision #1331)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_uses_new_vapid_endpoint() -> None:
|
||||||
|
"""app.js должен обращаться к /api/push/public-key (decision #1331)."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "/api/push/public-key" in content, (
|
||||||
|
"app.js не содержит endpoint '/api/push/public-key'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_does_not_use_old_vapid_endpoint() -> None:
|
||||||
|
"""app.js НЕ должен использовать устаревший /api/vapid-public-key (decision #1331)."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "/api/vapid-public-key" not in content, (
|
||||||
|
"app.js содержит устаревший endpoint '/api/vapid-public-key' — "
|
||||||
|
"нарушение decision #1331, должен использоваться '/api/push/public-key'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# app.js static analysis — Criterion 4: PushManager guard (decision #1332)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_has_push_manager_guard_in_registration_flow() -> None:
|
||||||
|
"""app.js должен содержать guard 'PushManager' in window (decision #1332)."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "'PushManager' in window" in content, (
|
||||||
|
"app.js не содержит guard \"'PushManager' in window\" — "
|
||||||
|
"нарушение decision #1332"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_push_manager_guard_combined_with_service_worker_check() -> None:
|
||||||
|
"""Guard PushManager должен сочетаться с проверкой serviceWorker."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
# Ищем паттерн совместной проверки serviceWorker + PushManager
|
||||||
|
assert re.search(
|
||||||
|
r"serviceWorker.*PushManager|PushManager.*serviceWorker",
|
||||||
|
content,
|
||||||
|
re.DOTALL,
|
||||||
|
), (
|
||||||
|
"app.js не содержит совместной проверки 'serviceWorker' и 'PushManager' — "
|
||||||
|
"guard неполный (decision #1332)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# app.js static analysis — Criterion 5: обработчик кнопки регистрации
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_has_handle_sign_up_function() -> None:
|
||||||
|
"""app.js должен содержать функцию _handleSignUp."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "_handleSignUp" in content, (
|
||||||
|
"app.js не содержит функцию '_handleSignUp'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_registers_click_handler_for_btn_register() -> None:
|
||||||
|
"""app.js должен добавлять click-обработчик на btn-register → _handleSignUp."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
# Ищем addEventListener на элементе btn-register с вызовом _handleSignUp
|
||||||
|
assert re.search(
|
||||||
|
r'btn-register.*addEventListener|addEventListener.*btn-register',
|
||||||
|
content,
|
||||||
|
re.DOTALL,
|
||||||
|
), (
|
||||||
|
"app.js не содержит addEventListener для кнопки 'btn-register'"
|
||||||
|
)
|
||||||
|
# Проверяем что именно _handleSignUp привязан к кнопке
|
||||||
|
assert re.search(
|
||||||
|
r'btn[Rr]egister.*_handleSignUp|_handleSignUp.*btn[Rr]egister',
|
||||||
|
content,
|
||||||
|
re.DOTALL,
|
||||||
|
), (
|
||||||
|
"app.js не связывает кнопку 'btn-register' с функцией '_handleSignUp'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# app.js static analysis — Criterion 6: переключение view-login / view-register
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_has_show_view_function() -> None:
|
||||||
|
"""app.js должен содержать функцию _showView для переключения видов."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "_showView" in content, (
|
||||||
|
"app.js не содержит функцию '_showView'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_show_view_handles_view_login() -> None:
|
||||||
|
"""_showView в app.js должна обрабатывать view-login."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "view-login" in content, (
|
||||||
|
"app.js не содержит id 'view-login' — нет переключения на вид логина"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_show_view_handles_view_register() -> None:
|
||||||
|
"""_showView в app.js должна обрабатывать view-register."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "view-register" in content, (
|
||||||
|
"app.js не содержит id 'view-register' — нет переключения на вид регистрации"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_has_btn_switch_to_register_handler() -> None:
|
||||||
|
"""app.js должен содержать обработчик для btn-switch-to-register."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "btn-switch-to-register" in content, (
|
||||||
|
"app.js не содержит ссылку на 'btn-switch-to-register'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_has_btn_switch_to_login_handler() -> None:
|
||||||
|
"""app.js должен содержать обработчик для btn-switch-to-login (назад)."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "btn-switch-to-login" in content, (
|
||||||
|
"app.js не содержит ссылку на 'btn-switch-to-login'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# app.js static analysis — Criterion 7: обработка ошибок / показ сообщения пользователю
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_has_set_reg_status_function() -> None:
|
||||||
|
"""app.js должен содержать _setRegStatus для показа статуса в форме регистрации."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "_setRegStatus" in content, (
|
||||||
|
"app.js не содержит функцию '_setRegStatus'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_handle_sign_up_shows_error_on_empty_fields() -> None:
|
||||||
|
"""_handleSignUp должна вызывать _setRegStatus с ошибкой при пустых полях."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
# Проверяем наличие валидации пустых полей внутри _handleSignUp-подобного блока
|
||||||
|
assert re.search(
|
||||||
|
r"_setRegStatus\s*\([^)]*error",
|
||||||
|
content,
|
||||||
|
), (
|
||||||
|
"app.js не содержит вызов _setRegStatus с классом 'error' "
|
||||||
|
"— ошибки не отображаются пользователю"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_handle_sign_up_shows_success_on_ok() -> None:
|
||||||
|
"""_handleSignUp должна вызывать _setRegStatus с success при успешной регистрации."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert re.search(
|
||||||
|
r"_setRegStatus\s*\([^)]*success",
|
||||||
|
content,
|
||||||
|
), (
|
||||||
|
"app.js не содержит вызов _setRegStatus с классом 'success' "
|
||||||
|
"— пользователь не уведомляется об успехе регистрации"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_clears_password_after_successful_signup() -> None:
|
||||||
|
"""_handleSignUp должна очищать поле пароля после успешной отправки."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
# Ищем сброс значения пароля
|
||||||
|
assert re.search(
|
||||||
|
r"passwordInput\.value\s*=\s*['\"][\s]*['\"]",
|
||||||
|
content,
|
||||||
|
), (
|
||||||
|
"app.js не очищает поле пароля после успешной регистрации — "
|
||||||
|
"пароль остаётся в DOM (security concern)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_js_uses_api_auth_register_endpoint() -> None:
|
||||||
|
"""app.js должен отправлять форму на /api/auth/register."""
|
||||||
|
content = APP_JS.read_text(encoding="utf-8")
|
||||||
|
assert "/api/auth/register" in content, (
|
||||||
|
"app.js не содержит endpoint '/api/auth/register'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Integration tests — API контракты (Criteria 8–12)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_vapid_public_key_endpoint_returns_200_with_key():
|
||||||
|
"""GET /api/push/public-key → 200 с полем vapid_public_key."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
resp = await client.get("/api/push/public-key")
|
||||||
|
|
||||||
|
assert resp.status_code == 200, (
|
||||||
|
f"GET /api/push/public-key вернул {resp.status_code}, ожидался 200"
|
||||||
|
)
|
||||||
|
body = resp.json()
|
||||||
|
assert "vapid_public_key" in body, (
|
||||||
|
f"Ответ /api/push/public-key не содержит 'vapid_public_key': {body}"
|
||||||
|
)
|
||||||
|
assert isinstance(body["vapid_public_key"], str), (
|
||||||
|
"vapid_public_key должен быть строкой"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_register_valid_payload_returns_201_pending():
|
||||||
|
"""POST /api/auth/register с валидными данными → 201 status=pending."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||||
|
resp = await client.post("/api/auth/register", json=_VALID_PAYLOAD)
|
||||||
|
|
||||||
|
assert resp.status_code == 201, (
|
||||||
|
f"POST /api/auth/register вернул {resp.status_code}: {resp.text}"
|
||||||
|
)
|
||||||
|
body = resp.json()
|
||||||
|
assert body.get("status") == "pending", (
|
||||||
|
f"Ожидался status='pending', получено: {body}"
|
||||||
|
)
|
||||||
|
assert "message" in body, (
|
||||||
|
f"Ответ не содержит поле 'message': {body}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_register_duplicate_email_returns_409():
|
||||||
|
"""POST /api/auth/register с дублирующим email → 409."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||||
|
r1 = await client.post("/api/auth/register", json=_VALID_PAYLOAD)
|
||||||
|
assert r1.status_code == 201, f"Первая регистрация не прошла: {r1.text}"
|
||||||
|
|
||||||
|
r2 = await client.post(
|
||||||
|
"/api/auth/register",
|
||||||
|
json={**_VALID_PAYLOAD, "login": "anotherlogin"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r2.status_code == 409, (
|
||||||
|
f"Дублирующий email должен вернуть 409, получено {r2.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_register_duplicate_login_returns_409():
|
||||||
|
"""POST /api/auth/register с дублирующим login → 409."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
with patch("backend.telegram.send_registration_notification", new_callable=AsyncMock):
|
||||||
|
r1 = await client.post("/api/auth/register", json=_VALID_PAYLOAD)
|
||||||
|
assert r1.status_code == 201, f"Первая регистрация не прошла: {r1.text}"
|
||||||
|
|
||||||
|
r2 = await client.post(
|
||||||
|
"/api/auth/register",
|
||||||
|
json={**_VALID_PAYLOAD, "email": "another@example.com"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r2.status_code == 409, (
|
||||||
|
f"Дублирующий login должен вернуть 409, получено {r2.status_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_register_invalid_email_returns_422():
|
||||||
|
"""POST /api/auth/register с невалидным email → 422."""
|
||||||
|
async with make_app_client() as client:
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/auth/register",
|
||||||
|
json={**_VALID_PAYLOAD, "email": "not-an-email"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 422, (
|
||||||
|
f"Невалидный email должен вернуть 422, получено {resp.status_code}"
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue