- 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>
96 lines
3.2 KiB
Python
96 lines
3.2 KiB
Python
"""Integration tests for the Evotor invite flow (/invite)."""
|
||
import secrets
|
||
from datetime import datetime, timezone, timedelta
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
|
||
from web.models.user import User, UserStatusEnum
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_invite_get_valid_token(client, override_db, user_factory):
|
||
token = secrets.token_urlsafe(32)
|
||
user = user_factory.create(
|
||
invite_token=token,
|
||
invite_expires=datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(hours=48),
|
||
password_hash=None,
|
||
status=UserStatusEnum.pending,
|
||
is_email_confirmed=False,
|
||
)
|
||
resp = await client.get(f"/invite?token={token}")
|
||
assert resp.status_code == 200
|
||
assert "Завершение регистрации" in resp.text or "ЭВОСИНК" in resp.text
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_invite_get_expired_token(client, user_factory):
|
||
token = secrets.token_urlsafe(32)
|
||
user_factory.create(
|
||
invite_token=token,
|
||
invite_expires=datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1),
|
||
password_hash=None,
|
||
status=UserStatusEnum.pending,
|
||
)
|
||
resp = await client.get(f"/invite?token={token}")
|
||
assert resp.status_code == 200
|
||
assert "недействительна" in resp.text
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_invite_get_bogus_token(client):
|
||
resp = await client.get("/invite?token=notexist")
|
||
assert resp.status_code == 200
|
||
assert "недействительна" in resp.text
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_invite_post_activates_user(client, override_db, user_factory):
|
||
token = secrets.token_urlsafe(32)
|
||
user = user_factory.create(
|
||
email="invite@test.com",
|
||
phone="+79001119999",
|
||
invite_token=token,
|
||
invite_expires=datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(hours=48),
|
||
password_hash=None,
|
||
status=UserStatusEnum.pending,
|
||
is_email_confirmed=False,
|
||
)
|
||
resp = await client.post(f"/invite?token={token}", data={
|
||
"first_name": "Петр",
|
||
"last_name": "Петров",
|
||
"email": "invite@test.com",
|
||
"phone": "+79001119999",
|
||
"password": "newpassword1",
|
||
"password_confirm": "newpassword1",
|
||
})
|
||
assert resp.status_code == 200
|
||
assert "активирован" in resp.text.lower()
|
||
|
||
override_db.refresh(user)
|
||
assert user.status == UserStatusEnum.active
|
||
assert user.is_email_confirmed is True
|
||
assert user.password_hash is not None
|
||
assert user.invite_token is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_invite_post_password_mismatch(client, user_factory):
|
||
token = secrets.token_urlsafe(32)
|
||
user_factory.create(
|
||
invite_token=token,
|
||
invite_expires=datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(hours=48),
|
||
password_hash=None,
|
||
status=UserStatusEnum.pending,
|
||
)
|
||
resp = await client.post(f"/invite?token={token}", data={
|
||
"first_name": "А",
|
||
"last_name": "Б",
|
||
"email": "x@test.com",
|
||
"phone": "+79001112233",
|
||
"password": "password123",
|
||
"password_confirm": "different",
|
||
})
|
||
assert resp.status_code == 200
|
||
assert "не совпадают" in resp.text
|