Merge branch 'BATON-008-backend_dev'

This commit is contained in:
Gros Frumos 2026-03-21 09:34:21 +02:00
commit c7661d7c1e
5 changed files with 48 additions and 20 deletions

View file

@ -22,7 +22,7 @@ WEBHOOK_ENABLED: bool = os.getenv("WEBHOOK_ENABLED", "true").lower() == "true"
FRONTEND_ORIGIN: str = os.getenv("FRONTEND_ORIGIN", "http://localhost:3000")
APP_URL: str | None = os.getenv("APP_URL") # Публичный URL приложения для keep-alive self-ping
ADMIN_TOKEN: str = _require("ADMIN_TOKEN")
ADMIN_CHAT_ID: str = os.getenv("ADMIN_CHAT_ID", "5694335584")
ADMIN_CHAT_ID: str = _require("ADMIN_CHAT_ID")
VAPID_PRIVATE_KEY: str = os.getenv("VAPID_PRIVATE_KEY", "")
VAPID_PUBLIC_KEY: str = os.getenv("VAPID_PUBLIC_KEY", "")
VAPID_CLAIMS_EMAIL: str = os.getenv("VAPID_CLAIMS_EMAIL", "")

View file

@ -341,9 +341,10 @@ async def get_registration(reg_id: int) -> Optional[dict]:
async def update_registration_status(reg_id: int, status: str) -> bool:
"""Update registration status only if currently 'pending'. Returns False if already processed."""
async with _get_conn() as conn:
async with conn.execute(
"UPDATE registrations SET status = ? WHERE id = ?",
"UPDATE registrations SET status = ? WHERE id = ? AND status = 'pending'",
(status, reg_id),
) as cur:
changed = cur.rowcount > 0

View file

@ -240,7 +240,11 @@ async def _handle_callback_query(cb: dict) -> None:
return
if action == "approve":
await db.update_registration_status(reg_id, "approved")
updated = await db.update_registration_status(reg_id, "approved")
if not updated:
# Already processed (not pending) — ack the callback and stop
await telegram.answer_callback_query(callback_query_id)
return
if chat_id and message_id:
await telegram.edit_message_text(
chat_id, message_id, f"✅ Пользователь {reg['login']} одобрен"
@ -254,7 +258,10 @@ async def _handle_callback_query(cb: dict) -> None:
)
)
elif action == "reject":
await db.update_registration_status(reg_id, "rejected")
updated = await db.update_registration_status(reg_id, "rejected")
if not updated:
await telegram.answer_callback_query(callback_query_id)
return
if chat_id and message_id:
await telegram.edit_message_text(
chat_id, message_id, f"❌ Пользователь {reg['login']} отклонён"

View file

@ -22,6 +22,7 @@ os.environ.setdefault("WEBHOOK_SECRET", "test-webhook-secret")
os.environ.setdefault("WEBHOOK_URL", "https://example.com/api/webhook/telegram")
os.environ.setdefault("FRONTEND_ORIGIN", "http://localhost:3000")
os.environ.setdefault("ADMIN_TOKEN", "test-admin-token")
os.environ.setdefault("ADMIN_CHAT_ID", "5694335584")
# ── 2. aiosqlite monkey-patch ────────────────────────────────────────────────
import aiosqlite
@ -69,14 +70,20 @@ def temp_db():
# ── 5. App client factory ────────────────────────────────────────────────────
def make_app_client():
def make_app_client(capture_send_requests: list | None = None):
"""
Async context manager that:
1. Assigns a fresh temp-file DB path
2. Mocks Telegram setWebhook, sendMessage, answerCallbackQuery, editMessageText
3. Runs the FastAPI lifespan (startup test shutdown)
4. Yields an httpx.AsyncClient wired to the app
Args:
capture_send_requests: if provided, each sendMessage request body (dict) is
appended to this list, enabling HTTP-level assertions on chat_id, text, etc.
"""
import json as _json
tg_set_url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/setWebhook"
send_url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendMessage"
get_me_url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/getMe"
@ -95,9 +102,18 @@ def make_app_client():
mock_router.post(tg_set_url).mock(
return_value=httpx.Response(200, json={"ok": True, "result": True})
)
mock_router.post(send_url).mock(
return_value=httpx.Response(200, json={"ok": True})
)
if capture_send_requests is not None:
def _capture_send(request: httpx.Request) -> httpx.Response:
try:
capture_send_requests.append(_json.loads(request.content))
except Exception:
pass
return httpx.Response(200, json={"ok": True})
mock_router.post(send_url).mock(side_effect=_capture_send)
else:
mock_router.post(send_url).mock(
return_value=httpx.Response(200, json={"ok": True})
)
mock_router.post(answer_cb_url).mock(
return_value=httpx.Response(200, json={"ok": True})
)

View file

@ -21,6 +21,7 @@ os.environ.setdefault("WEBHOOK_SECRET", "test-webhook-secret")
os.environ.setdefault("WEBHOOK_URL", "https://example.com/api/webhook/telegram")
os.environ.setdefault("FRONTEND_ORIGIN", "http://localhost:3000")
os.environ.setdefault("ADMIN_TOKEN", "test-admin-token")
os.environ.setdefault("ADMIN_CHAT_ID", "5694335584")
from unittest.mock import AsyncMock, patch
@ -167,20 +168,23 @@ async def test_auth_register_422_short_password():
@pytest.mark.asyncio
async def test_auth_register_sends_notification_to_admin():
"""Registration triggers send_registration_notification with correct data."""
calls: list[dict] = []
"""Registration triggers real HTTP sendMessage to ADMIN_CHAT_ID with correct login/email."""
from backend import config as _cfg
async def _capture(reg_id, login, email, created_at):
calls.append({"reg_id": reg_id, "login": login, "email": email})
captured: list[dict] = []
async with make_app_client(capture_send_requests=captured) as client:
resp = await client.post("/api/auth/register", json=_VALID_PAYLOAD)
assert resp.status_code == 201
await asyncio.sleep(0)
async with make_app_client() as client:
with patch("backend.telegram.send_registration_notification", side_effect=_capture):
await client.post("/api/auth/register", json=_VALID_PAYLOAD)
await asyncio.sleep(0)
assert len(calls) == 1, f"Expected 1 notification call, got {len(calls)}"
assert calls[0]["login"] == _VALID_PAYLOAD["login"]
assert calls[0]["email"] == _VALID_PAYLOAD["email"]
admin_chat_id = str(_cfg.ADMIN_CHAT_ID)
admin_msgs = [r for r in captured if str(r.get("chat_id")) == admin_chat_id]
assert len(admin_msgs) >= 1, (
f"Expected sendMessage to ADMIN_CHAT_ID={admin_chat_id!r}, captured: {captured}"
)
text = admin_msgs[0].get("text", "")
assert _VALID_PAYLOAD["login"] in text, f"Expected login in text: {text!r}"
assert _VALID_PAYLOAD["email"] in text, f"Expected email in text: {text!r}"
# ---------------------------------------------------------------------------