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>
This commit is contained in:
181
tests/test_routes_admin.py
Normal file
181
tests/test_routes_admin.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Integration tests for admin panel routes."""
|
||||
import pytest
|
||||
|
||||
from web.models.user import User, UserRoleEnum, UserStatusEnum
|
||||
|
||||
|
||||
def _set_session(client, user_id: int):
|
||||
"""Inject a session cookie so the client appears logged in as user_id."""
|
||||
client.cookies.set("session", "") # will be overwritten by actual login
|
||||
# We inject directly into the app's session via a helper request
|
||||
# The simplest approach: use the login endpoint to set the real session cookie
|
||||
return user_id
|
||||
|
||||
|
||||
async def _login(client, user):
|
||||
"""Log in as user via the /login endpoint to get a real session cookie."""
|
||||
resp = await client.post("/login", data={
|
||||
"email": user.email,
|
||||
"password": "testpass123",
|
||||
}, follow_redirects=False)
|
||||
assert resp.status_code == 303, f"Login failed: {resp.text}"
|
||||
|
||||
|
||||
# ── Access control ────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_users_requires_auth(client):
|
||||
resp = await client.get("/admin/users", follow_redirects=False)
|
||||
# Unauthenticated → redirect to login
|
||||
assert resp.status_code in (302, 303, 307)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_users_requires_admin_role(client, active_user):
|
||||
await _login(client, active_user)
|
||||
resp = await client.get("/admin/users", follow_redirects=False)
|
||||
# Regular user → redirect (not admin)
|
||||
assert resp.status_code in (302, 303, 307)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_users_accessible_by_admin(client, admin_user):
|
||||
await _login(client, admin_user)
|
||||
resp = await client.get("/admin/users")
|
||||
assert resp.status_code == 200
|
||||
assert "Пользователи" in resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_users_accessible_by_system(client, system_user):
|
||||
await _login(client, system_user)
|
||||
resp = await client.get("/admin/users")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ── User list + filters ───────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_users_shows_all_users(client, admin_user, user_factory):
|
||||
extra = user_factory.create(email="findme@test.com")
|
||||
await _login(client, admin_user)
|
||||
resp = await client.get("/admin/users")
|
||||
assert resp.status_code == 200
|
||||
assert extra.email in resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_users_search_filter(client, admin_user, user_factory):
|
||||
target = user_factory.create(email="searchable@test.com")
|
||||
await _login(client, admin_user)
|
||||
resp = await client.get("/admin/users?search=searchable")
|
||||
assert resp.status_code == 200
|
||||
assert target.email in resp.text
|
||||
|
||||
|
||||
# ── User detail ───────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_user_detail(client, admin_user, active_user):
|
||||
await _login(client, admin_user)
|
||||
resp = await client.get(f"/admin/users/{active_user.id}")
|
||||
assert resp.status_code == 200
|
||||
assert active_user.email in resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_user_detail_not_found(client, admin_user):
|
||||
await _login(client, admin_user)
|
||||
resp = await client.get("/admin/users/99999", follow_redirects=False)
|
||||
assert resp.status_code in (302, 303, 307)
|
||||
|
||||
|
||||
# ── Activate / suspend ────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_activate_user(client, admin_user, user_factory, override_db):
|
||||
target = user_factory.create(status=UserStatusEnum.suspended)
|
||||
await _login(client, admin_user)
|
||||
resp = await client.post(f"/admin/users/{target.id}/activate", follow_redirects=False)
|
||||
assert resp.status_code == 303
|
||||
override_db.refresh(target)
|
||||
assert target.status == UserStatusEnum.active
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_suspend_user(client, admin_user, active_user, override_db):
|
||||
await _login(client, admin_user)
|
||||
resp = await client.post(f"/admin/users/{active_user.id}/suspend", follow_redirects=False)
|
||||
assert resp.status_code == 303
|
||||
override_db.refresh(active_user)
|
||||
assert active_user.status == UserStatusEnum.suspended
|
||||
|
||||
|
||||
# ── Delete (system only) ──────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_delete_user_by_system(client, system_user, user_factory, override_db):
|
||||
target = user_factory.create()
|
||||
target_id = target.id
|
||||
await _login(client, system_user)
|
||||
resp = await client.post(f"/admin/users/{target_id}/delete", follow_redirects=False)
|
||||
assert resp.status_code == 303
|
||||
assert override_db.get(User, target_id) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_delete_blocked_for_admin_role(client, admin_user, active_user, override_db):
|
||||
target_id = active_user.id
|
||||
await _login(client, admin_user)
|
||||
resp = await client.post(f"/admin/users/{target_id}/delete", follow_redirects=False)
|
||||
assert resp.status_code == 303
|
||||
# Admin cannot delete — user still exists
|
||||
assert override_db.get(User, target_id) is not None
|
||||
|
||||
|
||||
# ── Reset password / send invite ──────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_reset_password_generates_token(client, admin_user, active_user, override_db):
|
||||
from unittest.mock import patch
|
||||
with patch("web.routes.admin.send_email_task") as mock_task:
|
||||
await _login(client, admin_user)
|
||||
resp = await client.post(
|
||||
f"/admin/users/{active_user.id}/reset-password", follow_redirects=False
|
||||
)
|
||||
assert resp.status_code == 303
|
||||
override_db.refresh(active_user)
|
||||
assert active_user.password_reset_token is not None
|
||||
mock_task.delay.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_send_invite(client, admin_user, active_user, override_db):
|
||||
from unittest.mock import patch
|
||||
with patch("web.routes.admin.send_email_task") as mock_task:
|
||||
await _login(client, admin_user)
|
||||
resp = await client.post(
|
||||
f"/admin/users/{active_user.id}/send-invite", follow_redirects=False
|
||||
)
|
||||
assert resp.status_code == 303
|
||||
override_db.refresh(active_user)
|
||||
assert active_user.invite_token is not None
|
||||
mock_task.delay.assert_called_once()
|
||||
|
||||
|
||||
# ── Roles page (system only) ──────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_roles_accessible_by_system(client, system_user):
|
||||
await _login(client, system_user)
|
||||
resp = await client.get("/admin/roles")
|
||||
assert resp.status_code == 200
|
||||
assert "Роли" in resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_roles_blocked_for_admin(client, admin_user):
|
||||
await _login(client, admin_user)
|
||||
resp = await client.get("/admin/roles", follow_redirects=False)
|
||||
# Admin is redirected away from roles page
|
||||
assert resp.status_code in (302, 303, 307)
|
||||
Reference in New Issue
Block a user