613 lines
22 KiB
Python
613 lines
22 KiB
Python
# =============================================================================
|
|
# 企微IT智能服务台 — 管理后台账号密码+OTP登录测试
|
|
# =============================================================================
|
|
# 覆盖范围:
|
|
# 1. 超级管理员初始化逻辑 (init_super_admin)
|
|
# 2. 账号密码登录流程 (/agents/login)
|
|
# 3. 管理员 CRUD API (/admin/users) — 注意:nginx已剥离/api前缀
|
|
# 4. 密码验证 (bcrypt)
|
|
# 5. OTP/MFA 二次验证
|
|
# =============================================================================
|
|
|
|
import os
|
|
import pytest
|
|
import pytest_asyncio
|
|
import pyotp
|
|
import bcrypt
|
|
from sqlalchemy import select
|
|
|
|
from app.models.agent import Agent
|
|
from app.models.role import Role
|
|
from app.models.user_role import UserRole
|
|
from app.services.admin_user_service import AdminUserService, init_super_admin
|
|
from tests.conftest import create_test_agent
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 辅助函数
|
|
# -----------------------------------------------------------------------------
|
|
async def _seed_admin_role(db_session, employee_id: str, role_name: str = "admin") -> str:
|
|
"""为用户分配指定角色."""
|
|
stmt = select(Role).where(Role.name == role_name)
|
|
role = (await db_session.execute(stmt)).scalars().first()
|
|
if not role:
|
|
role = Role(
|
|
id=str(__import__("uuid").uuid4()),
|
|
name=role_name,
|
|
display_name={"admin": "管理员", "super_admin": "超级管理员"}.get(role_name, role_name),
|
|
is_default=False,
|
|
permissions=[],
|
|
)
|
|
db_session.add(role)
|
|
await db_session.flush()
|
|
|
|
# 检查关联是否已存在
|
|
stmt = select(UserRole).where(
|
|
UserRole.employee_id == employee_id,
|
|
UserRole.role_id == role.id,
|
|
)
|
|
existing = (await db_session.execute(stmt)).scalars().first()
|
|
if not existing:
|
|
user_role = UserRole(
|
|
id=str(__import__("uuid").uuid4()),
|
|
employee_id=employee_id,
|
|
role_id=role.id,
|
|
source="manual",
|
|
assigned_at=__import__("datetime").datetime.now(),
|
|
)
|
|
db_session.add(user_role)
|
|
await db_session.flush()
|
|
|
|
return role.id
|
|
|
|
|
|
def _bearer(token: str) -> dict:
|
|
"""构造 Authorization header."""
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
async def _login_and_get_token(client, user_id: str, name: str, password: str = None, otp_code: str = None) -> dict:
|
|
"""调用 /agents/login 拿 token.
|
|
|
|
Returns:
|
|
dict: 包含 token, require_otp 等字段的响应数据
|
|
"""
|
|
json_body = {"user_id": user_id, "name": name}
|
|
if password:
|
|
json_body["password"] = password
|
|
if otp_code:
|
|
json_body["otp_code"] = otp_code
|
|
|
|
response = await client.post("/agents/login", json=json_body)
|
|
assert response.status_code == 200, f"登录失败: {response.text}"
|
|
body = response.json()
|
|
return body
|
|
|
|
|
|
# =============================================================================
|
|
# 1. 超级管理员初始化测试
|
|
# =============================================================================
|
|
class TestSuperAdminInit:
|
|
"""测试 init_super_admin 函数"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_super_admin_no_env_vars(self, db_session):
|
|
"""未配置环境变量时,返回 None,不创建用户"""
|
|
# 确保环境变量未设置
|
|
with pytest.MonkeyPatch.context() as mp:
|
|
mp.delenv("ADMIN_USERNAME", raising=False)
|
|
mp.delenv("ADMIN_PASSWORD", raising=False)
|
|
mp.delenv("ADMIN_NAME", raising=False)
|
|
|
|
result = await init_super_admin(db_session)
|
|
assert result is None
|
|
|
|
# 确认没有创建任何管理员
|
|
stmt = select(Agent).where(Agent.role == "super_admin")
|
|
result = await db_session.execute(stmt)
|
|
assert result.scalars().first() is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_super_admin_with_env_vars(self, db_session):
|
|
"""配置环境变量时,创建超级管理员"""
|
|
with pytest.MonkeyPatch.context() as mp:
|
|
mp.setenv("ADMIN_USERNAME", "superadmin")
|
|
mp.setenv("ADMIN_PASSWORD", "superpass123")
|
|
mp.setenv("ADMIN_NAME", "超级管理员")
|
|
|
|
result = await init_super_admin(db_session)
|
|
assert result is not None
|
|
assert result.user_id == "superadmin"
|
|
assert result.name == "超级管理员"
|
|
assert result.role == "super_admin"
|
|
|
|
# 验证密码已哈希存储
|
|
assert result.password_hash is not None
|
|
assert bcrypt.checkpw("superpass123".encode("utf-8"), result.password_hash.encode("utf-8"))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_super_admin_already_exists(self, db_session):
|
|
"""超级管理员已存在时,跳过创建"""
|
|
# 先创建一个
|
|
agent = create_test_agent(user_id="superadmin", name="超级管理员")
|
|
agent.role = "super_admin"
|
|
agent.password_hash = bcrypt.hashpw("oldpass".encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
|
db_session.add(agent)
|
|
await db_session.flush()
|
|
|
|
with pytest.MonkeyPatch.context() as mp:
|
|
mp.setenv("ADMIN_USERNAME", "superadmin")
|
|
mp.setenv("ADMIN_PASSWORD", "newpass123")
|
|
|
|
result = await init_super_admin(db_session)
|
|
assert result is not None
|
|
assert result.user_id == "superadmin"
|
|
# 密码应该是原来的,不应该被覆盖
|
|
assert bcrypt.checkpw("oldpass".encode("utf-8"), result.password_hash.encode("utf-8"))
|
|
|
|
|
|
# =============================================================================
|
|
# 2. AdminUserService 单元测试
|
|
# =============================================================================
|
|
class TestAdminUserService:
|
|
"""AdminUserService 静态方法直接测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_admin_user(self, db_session):
|
|
"""创建管理员用户"""
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="admin001",
|
|
name="管理员1",
|
|
role="admin",
|
|
password="test123456"
|
|
)
|
|
|
|
assert agent.user_id == "admin001"
|
|
assert agent.name == "管理员1"
|
|
assert agent.role == "admin"
|
|
assert bcrypt.checkpw("test123456".encode("utf-8"), agent.password_hash.encode("utf-8"))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_admin_user_duplicate(self, db_session):
|
|
"""重复创建管理员应抛出异常"""
|
|
service = AdminUserService(db_session)
|
|
await service.create_admin_user(
|
|
user_id="admin001",
|
|
name="管理员1",
|
|
role="admin",
|
|
password="test123456"
|
|
)
|
|
|
|
# 重复创建应抛出异常
|
|
from app.utils.error_codes import ErrorCode
|
|
from app.utils.response import AppException
|
|
with pytest.raises(AppException) as exc_info:
|
|
await service.create_admin_user(
|
|
user_id="admin001",
|
|
name="管理员1",
|
|
role="admin",
|
|
password="test123456"
|
|
)
|
|
assert "已存在" in str(exc_info.value.message)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_password_correct(self, db_session):
|
|
"""密码验证 - 正确密码"""
|
|
service = AdminUserService(db_session)
|
|
await service.create_admin_user(
|
|
user_id="admin002",
|
|
name="管理员2",
|
|
role="admin",
|
|
password="correctpassword"
|
|
)
|
|
|
|
agent = await service.verify_password("admin002", "correctpassword")
|
|
assert agent is not None
|
|
assert agent.user_id == "admin002"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_password_wrong(self, db_session):
|
|
"""密码验证 - 错误密码"""
|
|
service = AdminUserService(db_session)
|
|
await service.create_admin_user(
|
|
user_id="admin003",
|
|
name="管理员3",
|
|
role="admin",
|
|
password="correctpassword"
|
|
)
|
|
|
|
agent = await service.verify_password("admin003", "wrongpassword")
|
|
assert agent is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verify_password_nonexistent_user(self, db_session):
|
|
"""密码验证 - 不存在的用户"""
|
|
service = AdminUserService(db_session)
|
|
agent = await service.verify_password("nonexistent", "anypassword")
|
|
assert agent is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reset_password(self, db_session):
|
|
"""重置密码"""
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="admin004",
|
|
name="管理员4",
|
|
role="admin",
|
|
password="oldpassword"
|
|
)
|
|
|
|
updated = await service.reset_password(agent.id, "newpassword")
|
|
assert bcrypt.checkpw("newpassword".encode("utf-8"), updated.password_hash.encode("utf-8"))
|
|
assert not bcrypt.checkpw("oldpassword".encode("utf-8"), updated.password_hash.encode("utf-8"))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_admin_user(self, db_session):
|
|
"""删除管理员"""
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="admin005",
|
|
name="管理员5",
|
|
role="admin",
|
|
password="test123"
|
|
)
|
|
|
|
result = await service.delete_admin_user(agent.id)
|
|
assert result is True
|
|
|
|
# 验证已删除
|
|
deleted = await service.get_user_by_id(agent.id)
|
|
assert deleted is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_super_admin_forbidden(self, db_session):
|
|
"""删除超级管理员应被拒绝"""
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="superadmin",
|
|
name="超级管理员",
|
|
role="super_admin",
|
|
password="test123"
|
|
)
|
|
|
|
from app.utils.error_codes import ErrorCode
|
|
from app.utils.response import AppException
|
|
with pytest.raises(AppException) as exc_info:
|
|
await service.delete_admin_user(agent.id)
|
|
assert "超级管理员" in str(exc_info.value.message)
|
|
|
|
|
|
# =============================================================================
|
|
# 3. 管理员 CRUD API 测试
|
|
# =============================================================================
|
|
class TestAdminUserAPI:
|
|
"""管理员用户 CRUD API 测试
|
|
|
|
注意: nginx 配置会将 /api 前缀剥离,所以实际路径是 /admin/users 而非 /api/admin/users
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_admin_users(self, client, db_session):
|
|
"""GET /admin/users - 获取管理员列表"""
|
|
# 创建测试管理员
|
|
service = AdminUserService(db_session)
|
|
await service.create_admin_user("admin_list_1", "管理员A", "admin", "pass123")
|
|
await service.create_admin_user("admin_list_2", "管理员B", "admin", "pass456")
|
|
await db_session.commit()
|
|
|
|
# 创建管理员用户并分配角色
|
|
admin_agent = create_test_agent(user_id="test_admin_user", name="测试管理员")
|
|
admin_agent.role = "admin"
|
|
db_session.add(admin_agent)
|
|
await db_session.flush()
|
|
await _seed_admin_role(db_session, "test_admin_user", "admin")
|
|
|
|
# 登录获取 token
|
|
login_resp = await _login_and_get_token(client, "test_admin_user", "测试管理员")
|
|
token = login_resp["data"]["token"]
|
|
|
|
# 调用 API (无 /api 前缀)
|
|
response = await client.get("/admin/users", headers=_bearer(token))
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["code"] == 0
|
|
assert body["data"]["total"] >= 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_admin_user_api(self, client, db_session):
|
|
"""POST /admin/users - 创建管理员"""
|
|
# 创建超级管理员
|
|
super_agent = create_test_agent(user_id="test_super_admin", name="测试超级管理员")
|
|
super_agent.role = "super_admin"
|
|
db_session.add(super_agent)
|
|
await db_session.flush()
|
|
await _seed_admin_role(db_session, "test_super_admin", "super_admin")
|
|
|
|
# 登录获取 token
|
|
login_resp = await _login_and_get_token(client, "test_super_admin", "测试超级管理员")
|
|
token = login_resp["data"]["token"]
|
|
|
|
# 创建管理员
|
|
response = await client.post(
|
|
"/admin/users",
|
|
headers=_bearer(token),
|
|
json={
|
|
"user_id": "new_admin",
|
|
"name": "新管理员",
|
|
"role": "admin",
|
|
"password": "newpass123"
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["code"] == 0
|
|
assert body["data"]["user_id"] == "new_admin"
|
|
assert body["data"]["role"] == "admin"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_admin_user(self, client, db_session):
|
|
"""GET /admin/users/{id} - 获取管理员详情"""
|
|
# 创建管理员
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user("admin_detail", "管理员详情", "admin", "pass123")
|
|
await db_session.commit()
|
|
|
|
# 查询用户并分配角色
|
|
stmt = select(Agent).where(Agent.user_id == "admin_detail")
|
|
result = await db_session.execute(stmt)
|
|
admin_agent = result.scalars().first()
|
|
|
|
# 分配 admin 角色
|
|
await _seed_admin_role(db_session, "admin_detail", "admin")
|
|
|
|
# 登录获取 token
|
|
login_resp = await _login_and_get_token(client, "admin_detail", "管理员详情")
|
|
token = login_resp["data"]["token"]
|
|
|
|
# 获取详情
|
|
response = await client.get(f"/admin/users/{admin_agent.id}", headers=_bearer(token))
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["code"] == 0
|
|
assert body["data"]["user_id"] == "admin_detail"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_admin_user(self, client, db_session):
|
|
"""PUT /admin/users/{id} - 更新管理员"""
|
|
# 创建管理员
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user("admin_update", "管理员更新", "admin", "pass123")
|
|
await db_session.commit()
|
|
|
|
# 分配角色
|
|
await _seed_admin_role(db_session, "admin_update", "admin")
|
|
|
|
# 登录获取 token
|
|
login_resp = await _login_and_get_token(client, "admin_update", "管理员更新")
|
|
token = login_resp["data"]["token"]
|
|
|
|
# 更新管理员
|
|
response = await client.put(
|
|
f"/admin/users/{agent.id}",
|
|
headers=_bearer(token),
|
|
json={"name": "新名字", "is_active": True}
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["code"] == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_admin_user_api(self, client, db_session):
|
|
"""DELETE /admin/users/{id} - 删除管理员"""
|
|
# 创建超级管理员
|
|
super_agent = create_test_agent(user_id="test_super_delete", name="测试超级管理员")
|
|
super_agent.role = "super_admin"
|
|
db_session.add(super_agent)
|
|
await db_session.flush()
|
|
await _seed_admin_role(db_session, "test_super_delete", "super_admin")
|
|
|
|
# 创建待删除的管理员
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user("admin_to_delete", "待删除管理员", "admin", "pass123")
|
|
await db_session.commit()
|
|
|
|
# 登录获取 token
|
|
login_resp = await _login_and_get_token(client, "test_super_delete", "测试超级管理员")
|
|
token = login_resp["data"]["token"]
|
|
|
|
# 删除管理员
|
|
response = await client.delete(f"/admin/users/{agent.id}", headers=_bearer(token))
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["code"] == 0
|
|
|
|
|
|
# =============================================================================
|
|
# 4. 账号密码+OTP登录测试
|
|
# =============================================================================
|
|
class TestPasswordOTPLogin:
|
|
"""账号密码 + OTP 登录流程测试
|
|
|
|
登录流程说明:
|
|
1. 优先尝试企微通讯录验证
|
|
2. 企微不可达时,已注册坐席可降级登录(需验证本地密码)
|
|
3. 启用MFA后需要OTP验证
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_without_password(self, client, db_session):
|
|
"""登录 - 无密码的新坐席(企微验证通过)"""
|
|
# 创建管理员但不设置密码
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="login_test_user",
|
|
name="登录测试用户",
|
|
role="admin"
|
|
# 不设置 password
|
|
)
|
|
await db_session.commit()
|
|
|
|
# 登录(企微验证通过)
|
|
response = await _login_and_get_token(
|
|
client,
|
|
user_id="login_test_user",
|
|
name="登录测试用户"
|
|
)
|
|
|
|
body = response
|
|
assert body["code"] == 0
|
|
assert "token" in body["data"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_require_otp_when_mfa_enabled(self, client, db_session):
|
|
"""登录 - MFA 启用时需要 OTP 验证"""
|
|
# 创建管理员并启用 MFA
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="mfa_user",
|
|
name="MFA用户",
|
|
role="admin",
|
|
password="testpassword123"
|
|
)
|
|
# 模拟已绑定 MFA
|
|
secret = pyotp.random_base32()
|
|
agent.mfa_secret = secret
|
|
agent.mfa_enabled = True
|
|
await db_session.commit()
|
|
|
|
# 登录但不提供 OTP
|
|
response = await _login_and_get_token(
|
|
client,
|
|
user_id="mfa_user",
|
|
name="MFA用户",
|
|
password="testpassword123"
|
|
)
|
|
|
|
body = response
|
|
assert body["code"] == 0
|
|
assert body["data"]["require_otp"] is True
|
|
assert "token" not in body["data"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_with_correct_otp(self, client, db_session):
|
|
"""登录 - 提供正确的 OTP"""
|
|
# 创建管理员并启用 MFA
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="otp_user",
|
|
name="OTP用户",
|
|
role="admin",
|
|
password="testpassword123"
|
|
)
|
|
# 模拟已绑定 MFA
|
|
secret = pyotp.random_base32()
|
|
agent.mfa_secret = secret
|
|
agent.mfa_enabled = True
|
|
await db_session.commit()
|
|
|
|
# 生成当前有效的 OTP
|
|
totp = pyotp.TOTP(secret)
|
|
otp_code = totp.now()
|
|
|
|
# 登录并提供 OTP
|
|
response = await _login_and_get_token(
|
|
client,
|
|
user_id="otp_user",
|
|
name="OTP用户",
|
|
password="testpassword123",
|
|
otp_code=otp_code
|
|
)
|
|
|
|
body = response
|
|
assert body["code"] == 0
|
|
assert "token" in body["data"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_with_wrong_otp(self, client, db_session):
|
|
"""登录 - 提供错误的 OTP"""
|
|
# 创建管理员并启用 MFA
|
|
service = AdminUserService(db_session)
|
|
agent = await service.create_admin_user(
|
|
user_id="wrong_otp_user",
|
|
name="错误OTP用户",
|
|
role="admin",
|
|
password="testpassword123"
|
|
)
|
|
# 模拟已绑定 MFA
|
|
secret = pyotp.random_base32()
|
|
agent.mfa_secret = secret
|
|
agent.mfa_enabled = True
|
|
await db_session.commit()
|
|
|
|
# 使用错误的 OTP 登录
|
|
response = await _login_and_get_token(
|
|
client,
|
|
user_id="wrong_otp_user",
|
|
name="错误OTP用户",
|
|
password="testpassword123",
|
|
otp_code="000000"
|
|
)
|
|
|
|
# 应该返回业务错误
|
|
assert response["code"] != 0
|
|
|
|
|
|
# =============================================================================
|
|
# 5. 权限控制测试
|
|
# =============================================================================
|
|
class TestAdminPermission:
|
|
"""管理员权限控制测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_admin_requires_super_admin(self, client, db_session):
|
|
"""创建管理员需要 super_admin 角色"""
|
|
# 创建普通管理员
|
|
normal_admin = create_test_agent(user_id="normal_admin_perm", name="普通管理员")
|
|
normal_admin.role = "admin"
|
|
db_session.add(normal_admin)
|
|
await db_session.flush()
|
|
await _seed_admin_role(db_session, "normal_admin_perm", "admin")
|
|
|
|
# 登录获取 token
|
|
login_resp = await _login_and_get_token(client, "normal_admin_perm", "普通管理员")
|
|
token = login_resp["data"]["token"]
|
|
|
|
# 尝试创建管理员 (无 /api 前缀)
|
|
response = await client.post(
|
|
"/admin/users",
|
|
headers=_bearer(token),
|
|
json={
|
|
"user_id": "should_fail",
|
|
"name": "应该失败",
|
|
"role": "admin"
|
|
}
|
|
)
|
|
# 应该返回非0业务码
|
|
body = response.json()
|
|
assert body["code"] != 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_admin_requires_super_admin(self, client, db_session):
|
|
"""删除管理员需要 super_admin 角色"""
|
|
# 创建普通管理员
|
|
normal_admin = create_test_agent(user_id="normal_admin_del", name="普通管理员")
|
|
normal_admin.role = "admin"
|
|
db_session.add(normal_admin)
|
|
await db_session.flush()
|
|
await _seed_admin_role(db_session, "normal_admin_del", "admin")
|
|
|
|
# 创建待删除的管理员
|
|
service = AdminUserService(db_session)
|
|
to_delete = await service.create_admin_user("target_admin", "目标管理员", "admin", "pass123")
|
|
await db_session.commit()
|
|
|
|
# 登录获取 token
|
|
login_resp = await _login_and_get_token(client, "normal_admin_del", "普通管理员")
|
|
token = login_resp["data"]["token"]
|
|
|
|
# 尝试删除管理员 (无 /api 前缀)
|
|
response = await client.delete(f"/admin/users/{to_delete.id}", headers=_bearer(token))
|
|
body = response.json()
|
|
assert body["code"] != 0
|