Files
wecom_it_smart_desk/backend/tests/test_response_contract.py
T

226 lines
7.4 KiB
Python
Raw Normal View History

# =============================================================================
# 三端认证重构 CTRT — 响应契约统一测试
# =============================================================================
# 验证后端响应格式符合统一信封规范:
# - 成功响应: {code: 0, data: {...}, message: "success"}
# - 失败响应: {code: 非0, data: null, message: "..."}
#
# 前端拦截器会根据 code 判断,成功时返回内层 dataCTRT-01
# 失败时抛出错对象 {code, message}CTRT-03
# =============================================================================
import pytest
from tests.conftest import create_test_agent
# =============================================================================
# 辅助函数
# =============================================================================
async def _login_and_get_token(client, user_id: str, name: str, otp_code: str = None) -> str:
"""登录获取 token
Args:
client: 测试客户端
user_id: 用户 ID
name: 用户名
otp_code: 可选的 OTP 验证码
"""
payload = {"user_id": user_id, "name": name}
if otp_code:
payload["otp_code"] = otp_code
response = await client.post(
"/agents/login",
json=payload,
)
assert response.status_code == 200
body = response.json()
# 如果返回 require_otp,说明需要 OTP 验证
if body["data"].get("require_otp"):
return None
return body["data"]["token"]
def _bearer(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
# =============================================================================
# 响应格式测试
# =============================================================================
class TestResponseEnvelope:
"""测试统一响应信封格式"""
@pytest.mark.asyncio
async def test_success_response_format(self, client, db_session):
"""成功响应应符合 {code: 0, data: {...}, message: "success"} 格式"""
# 创建一个坐席
agent = create_test_agent(user_id="test_agent_001", name="TestAgent")
db_session.add(agent)
await db_session.flush()
# 登录
token = await _login_and_get_token(client, "test_agent_001", "TestAgent")
# 调用需要认证的端点
response = await client.get("/agents/me", headers=_bearer(token))
assert response.status_code == 200
body = response.json()
# 验证响应格式
assert "code" in body
assert "data" in body
assert "message" in body
assert body["code"] == 0
assert body["message"] == "success"
@pytest.mark.asyncio
async def test_error_response_format(self, client):
"""错误响应应符合 {code: 非0, data: null, message: "..."} 格式"""
# 使用无效 token 调用
response = await client.get(
"/agents/me",
headers=_bearer("invalid_token_12345")
)
# 401 会返回统一错误格式
assert response.status_code in [200, 401]
body = response.json()
# 验证错误响应格式
assert "code" in body
assert "message" in body
# code 不为 0
assert body["code"] != 0
class TestOTPResponseContract:
"""测试 OTP 端点响应契约"""
@pytest.mark.asyncio
async def test_otp_status_response(self, client, db_session):
"""OTP status 端点返回内层 data"""
agent = create_test_agent(user_id="otp_test_001", name="OTPTest")
db_session.add(agent)
await db_session.flush()
token = await _login_and_get_token(client, "otp_test_001", "OTPTest")
response = await client.get("/auth/otp-status", headers=_bearer(token))
assert response.status_code == 200
body = response.json()
# 验证统一格式
assert body["code"] == 0
assert body["data"]["bound"] is False
assert body["data"]["enabled"] is False
@pytest.mark.asyncio
async def test_otp_bind_response(self, client, db_session):
"""OTP bind 端点返回内层 data"""
agent = create_test_agent(user_id="otp_test_002", name="OTPTest2")
db_session.add(agent)
await db_session.flush()
token = await _login_and_get_token(client, "otp_test_002", "OTPTest2")
response = await client.post("/auth/otp-bind", headers=_bearer(token))
assert response.status_code == 200
body = response.json()
# 验证统一格式
assert body["code"] == 0
assert "secret" in body["data"]
assert "otpauth_url" in body["data"]
assert "qr_code_base64" in body["data"]
@pytest.mark.asyncio
async def test_otp_verify_response(self, client, db_session):
"""OTP verify 端点返回内层 data"""
from datetime import datetime
import pyotp
agent = create_test_agent(user_id="otp_test_003", name="OTPTest3")
secret = pyotp.random_base32()
agent.mfa_secret = secret
agent.mfa_enabled = True
agent.mfa_bound_at = datetime.now()
db_session.add(agent)
await db_session.flush()
# 已启用 MFA 的用户需要使用 OTP 验证码登录
otp_code = pyotp.TOTP(secret).now()
token = await _login_and_get_token(client, "otp_test_003", "OTPTest3", otp_code)
response = await client.post(
"/auth/otp-verify",
headers=_bearer(token),
json={"otp_code": otp_code},
)
assert response.status_code == 200
body = response.json()
# 验证统一格式
assert body["code"] == 0
assert body["data"]["verified"] is True
class TestAgentsLoginResponse:
"""测试登录响应契约"""
@pytest.mark.asyncio
async def test_login_without_mfa_returns_token(self, client, db_session):
"""无 MFA 时登录直接返回 token"""
agent = create_test_agent(user_id="login_test_001", name="LoginTest")
db_session.add(agent)
await db_session.flush()
response = await client.post(
"/agents/login",
json={"user_id": "login_test_001", "name": "LoginTest"}
)
assert response.status_code == 200
body = response.json()
# 验证统一格式
assert body["code"] == 0
assert "token" in body["data"]
assert "user_id" in body["data"]
assert "name" in body["data"]
@pytest.mark.asyncio
async def test_login_with_mfa_requires_otp(self, client, db_session):
"""有 MFA 时登录返回 require_otp"""
from datetime import datetime
import pyotp
agent = create_test_agent(user_id="login_test_002", name="LoginTest2")
secret = pyotp.random_base32()
agent.mfa_secret = secret
agent.mfa_enabled = True
agent.mfa_bound_at = datetime.now()
db_session.add(agent)
await db_session.flush()
# 登录时不带 otp_code
response = await client.post(
"/agents/login",
json={"user_id": "login_test_002", "name": "LoginTest2"}
)
assert response.status_code == 200
body = response.json()
# 应该返回 require_otp: true(不带 token
assert body["code"] == 0
assert body["data"].get("require_otp") is True
assert "token" not in body["data"]
if __name__ == "__main__":
pytest.main([__file__, "-v"])