Files
Aether/tests/api/test_admin_user_routes.py
fawney19 c4bb6b8161 feat(auth): 重构认证系统,引入 session 会话管理
- 新增 user_sessions 数据库表及 Alembic 迁移
- 实现 SessionService 会话生命周期管理(创建/刷新/撤销/清理)
- 认证流程改用 refresh token cookie + access token 双令牌模式
- 前端实现自动静默刷新、跨标签页同步及设备指纹
- 用户设置页新增会话管理和密码修改功能
- 管理员用户管理新增强制登出和会话查看
- 密码策略增强,支持强度校验和泄露检测
- OAuth 登录流程适配新会话机制
- 新增完整的单元测试和 API 测试覆盖

Closes #232

Co-authored-by: LewisPen <LewisPen@nyadoo.com>
2026-03-17 16:34:09 +08:00

198 lines
6.4 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.admin.users.routes import AdminCreateUserAdapter
from src.api.admin.users.routes import router as admin_users_router
from src.database import get_db
def _build_admin_users_app(db: MagicMock, monkeypatch: pytest.MonkeyPatch) -> TestClient:
app = FastAPI()
app.include_router(admin_users_router)
app.dependency_overrides[get_db] = lambda: db
async def _fake_pipeline_run(
*, adapter: Any, http_request: object, db: MagicMock, mode: object
) -> Any:
_ = http_request, mode
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
user=SimpleNamespace(id="admin-1"),
ensure_json_body=lambda: {},
add_audit_metadata=lambda **_: None,
)
return await adapter.handle(context)
monkeypatch.setattr("src.api.admin.users.routes.pipeline.run", _fake_pipeline_run)
return TestClient(app)
def test_list_users_uses_wallet_batch_lookup(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
now = datetime.now(timezone.utc)
users = [
SimpleNamespace(
id="user-1",
email="u1@example.com",
username="user1",
role=SimpleNamespace(value="user"),
allowed_providers=None,
allowed_api_formats=None,
allowed_models=None,
rate_limit=None,
is_active=True,
created_at=now,
updated_at=now,
last_login_at=None,
),
SimpleNamespace(
id="user-2",
email="u2@example.com",
username="user2",
role=SimpleNamespace(value="admin"),
allowed_providers=None,
allowed_api_formats=None,
allowed_models=None,
rate_limit=None,
is_active=True,
created_at=now,
updated_at=None,
last_login_at=None,
),
]
wallets_by_user_id = {
"user-1": SimpleNamespace(limit_mode="unlimited"),
}
batch_getter = MagicMock(return_value=wallets_by_user_id)
monkeypatch.setattr(
"src.api.admin.users.routes.UserService.list_users", lambda *_a, **_k: users
)
monkeypatch.setattr(
"src.api.admin.users.routes.WalletService.get_wallets_by_user_ids",
batch_getter,
)
monkeypatch.setattr(
"src.api.admin.users.routes.WalletService.get_wallet",
lambda *_a, **_k: (_ for _ in ()).throw(AssertionError("不应回退到逐个钱包查询")),
)
response = client.get("/api/admin/users")
assert response.status_code == 200
assert response.json()[0]["unlimited"] is True
assert response.json()[1]["unlimited"] is False
batch_getter.assert_called_once()
assert batch_getter.call_args.args[1] == ["user-1", "user-2"]
def test_list_user_sessions_route_returns_sessions(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
sessions = [
{
"id": "session-1",
"device_label": "Chrome / macOS",
"device_type": "desktop",
"created_at": datetime.now(timezone.utc).isoformat(),
"is_current": False,
}
]
monkeypatch.setattr(
"src.api.admin.users.routes._list_user_sessions_sync",
lambda user_id: (
sessions,
{"action": "list_user_sessions", "target_user_id": user_id},
),
)
response = client.get("/api/admin/users/user-1/sessions")
assert response.status_code == 200
assert response.json() == sessions
def test_revoke_user_session_route_returns_message(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
monkeypatch.setattr(
"src.api.admin.users.routes._revoke_user_session_sync",
lambda user_id, session_id, admin_user_id: (
{"message": f"{user_id}:{session_id}:revoked"},
{"action": "revoke_user_session", "target_user_id": user_id, "session_id": session_id},
),
)
response = client.delete("/api/admin/users/user-1/sessions/session-1")
assert response.status_code == 200
assert response.json() == {"message": "user-1:session-1:revoked"}
def test_revoke_all_user_sessions_route_returns_count(monkeypatch: pytest.MonkeyPatch) -> None:
db = MagicMock()
client = _build_admin_users_app(db, monkeypatch)
monkeypatch.setattr(
"src.api.admin.users.routes._revoke_all_user_sessions_sync",
lambda user_id, admin_user_id: (
{"message": "done", "revoked_count": 2},
{"action": "revoke_all_user_sessions", "target_user_id": user_id, "revoked_count": 2},
),
)
response = client.delete("/api/admin/users/user-1/sessions")
assert response.status_code == 200
assert response.json() == {"message": "done", "revoked_count": 2}
@pytest.mark.asyncio
async def test_create_user_adapter_preserves_empty_restriction_lists(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
captured: dict[str, Any] = {}
def _fake_create_user_sync(request: Any, role: Any) -> tuple[dict[str, Any], dict[str, Any]]:
captured["request"] = request
captured["role"] = role
return {"id": "user-3"}, {"action": "create_user", "target_user_id": "user-3"}
monkeypatch.setattr("src.api.admin.users.routes._create_user_sync", _fake_create_user_sync)
context = SimpleNamespace(
db=db,
request=SimpleNamespace(state=SimpleNamespace()),
ensure_json_body=lambda: {
"username": "user3",
"password": "Abcd12",
"email": "u3@example.com",
"role": "user",
"initial_gift_usd": 10,
"allowed_providers": [],
"allowed_api_formats": [],
"allowed_models": [],
},
add_audit_metadata=lambda **_: None,
)
result = await AdminCreateUserAdapter().handle(context)
assert result == {"id": "user-3"}
assert captured["request"].allowed_providers == []
assert captured["request"].allowed_api_formats == []
assert captured["request"].allowed_models == []