Files
evo-sync/tests/test_routes_auth.py
mguschin fc65e591b3 test: add test suite with 65 tests, 73% coverage
- Unit tests: password hashing, notification providers, webhook field parsing
- Integration tests: auth routes (register/login/confirm-email/logout),
  invite flow, Evotor webhooks (/user/create, /user/verify, /user/token),
  admin panel (access control, activate/suspend/delete/reset-password)
- conftest: SQLite in-memory engine, transactional sessions, factory-boy
  factories (UserFactory with UserRoleEnum variants)
- Fix bcrypt: replace passlib (broken on Python 3.14 + bcrypt 5.x) with
  direct bcrypt calls; drop passlib from requirements.txt
- Fix datetime.utcnow() deprecation across routes and tests
- Fix Jinja2 TemplateResponse signature (request as first positional arg)
- Add coverage config to pyproject.toml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 12:27:42 +03:00

183 lines
6.1 KiB
Python

"""Integration tests for auth routes (register / login / confirm-email / logout)."""
import secrets
from unittest.mock import patch
import pytest
from web.models.user import User, UserStatusEnum
# ── /register ────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_register_get(client):
resp = await client.get("/register")
assert resp.status_code == 200
assert "Регистрация" in resp.text
@pytest.mark.asyncio
@patch("web.routes.auth.send_email_task")
async def test_register_creates_pending_user(mock_task, client, override_db):
resp = await client.post("/register", data={
"first_name": "Иван",
"last_name": "Иванов",
"email": "ivan@test.com",
"phone": "+79001234567",
"password": "password123",
"password_confirm": "password123",
})
assert resp.status_code == 200
assert "Подтвердите" in resp.text
user = override_db.query(User).filter(User.email == "ivan@test.com").first()
assert user is not None
assert user.status == UserStatusEnum.pending
assert user.is_email_confirmed is False
assert user.email_confirm_token is not None
mock_task.delay.assert_called_once()
@pytest.mark.asyncio
@patch("web.routes.auth.send_email_task")
async def test_register_duplicate_email(mock_task, client, active_user):
resp = await client.post("/register", data={
"first_name": "X",
"last_name": "Y",
"email": active_user.email,
"phone": "+79999999999",
"password": "password123",
"password_confirm": "password123",
})
assert resp.status_code == 200
assert "уже существует" in resp.text
mock_task.delay.assert_not_called()
@pytest.mark.asyncio
async def test_register_password_mismatch(client):
resp = await client.post("/register", data={
"email": "new@test.com",
"phone": "+79000000001",
"password": "password123",
"password_confirm": "different",
})
assert resp.status_code == 200
assert "не совпадают" in resp.text
@pytest.mark.asyncio
async def test_register_short_password(client):
resp = await client.post("/register", data={
"email": "new@test.com",
"phone": "+79000000002",
"password": "short",
"password_confirm": "short",
})
assert resp.status_code == 200
assert "минимум 8" in resp.text
# ── /confirm-email ────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_confirm_email_valid_token(client, override_db, user_factory):
token = secrets.token_urlsafe(32)
user = user_factory.create(
is_email_confirmed=False,
email_confirm_token=token,
status=UserStatusEnum.pending,
)
resp = await client.get(f"/confirm-email?token={token}")
assert resp.status_code == 200
assert "подтвержден" in resp.text.lower()
override_db.refresh(user)
assert user.is_email_confirmed is True
assert user.status == UserStatusEnum.active
assert user.email_confirm_token is None
@pytest.mark.asyncio
async def test_confirm_email_invalid_token(client):
resp = await client.get("/confirm-email?token=bogustoken")
assert resp.status_code == 200
assert "Ошибка" in resp.text
@pytest.mark.asyncio
async def test_confirm_email_missing_token(client):
resp = await client.get("/confirm-email")
assert resp.status_code == 200
assert "Ошибка" in resp.text
# ── /login ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_login_get(client):
resp = await client.get("/login")
assert resp.status_code == 200
assert "Вход" in resp.text
@pytest.mark.asyncio
async def test_login_success(client, active_user):
resp = await client.post("/login", data={
"email": active_user.email,
"password": "testpass123",
}, follow_redirects=False)
assert resp.status_code == 303
assert resp.headers["location"] == "/profile"
@pytest.mark.asyncio
async def test_login_wrong_password(client, active_user):
resp = await client.post("/login", data={
"email": active_user.email,
"password": "wrongpassword",
})
assert resp.status_code == 200
assert "Неверный" in resp.text
@pytest.mark.asyncio
async def test_login_unknown_email(client):
resp = await client.post("/login", data={
"email": "nobody@test.com",
"password": "testpass123",
})
assert resp.status_code == 200
assert "Неверный" in resp.text
@pytest.mark.asyncio
async def test_login_suspended_user(client, user_factory):
user = user_factory.create(status=UserStatusEnum.suspended)
resp = await client.post("/login", data={
"email": user.email,
"password": "testpass123",
})
assert resp.status_code == 200
assert "заблокирован" in resp.text.lower()
@pytest.mark.asyncio
async def test_login_unconfirmed_email(client, user_factory):
user = user_factory.create(is_email_confirmed=False, status=UserStatusEnum.pending)
resp = await client.post("/login", data={
"email": user.email,
"password": "testpass123",
})
assert resp.status_code == 200
assert "подтвердите" in resp.text.lower()
# ── /logout ───────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_logout_redirects(client):
resp = await client.get("/logout", follow_redirects=False)
assert resp.status_code == 303
assert resp.headers["location"] == "/login"