fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,100 +1,260 @@
|
||||
"""
|
||||
用户认证API测试
|
||||
|
||||
覆盖 JWT 签发/验证/过期 + 401 拦截 + FormData 修复验证
|
||||
"""
|
||||
import time
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from jose import jwt
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.auth
|
||||
class TestAuth:
|
||||
"""认证相关测试"""
|
||||
|
||||
|
||||
def test_register_user(self, client, test_user_data):
|
||||
"""测试用户注册"""
|
||||
response = client.post("/api/v1/auth/register", json=test_user_data)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
data = response.json()
|
||||
assert "id" in data
|
||||
assert data["username"] == test_user_data["username"]
|
||||
assert data["email"] == test_user_data["email"]
|
||||
assert "password_hash" not in data # 密码哈希不应该返回
|
||||
|
||||
assert "password_hash" not in data
|
||||
|
||||
def test_register_duplicate_username(self, client, test_user_data):
|
||||
"""测试重复用户名注册"""
|
||||
# 第一次注册
|
||||
response1 = client.post("/api/v1/auth/register", json=test_user_data)
|
||||
assert response1.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# 第二次注册相同用户名
|
||||
response2 = client.post("/api/v1/auth/register", json=test_user_data)
|
||||
assert response2.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
assert response2.status_code in (status.HTTP_400_BAD_REQUEST, status.HTTP_409_CONFLICT)
|
||||
|
||||
def test_register_duplicate_email(self, client, test_user_data):
|
||||
"""测试重复邮箱注册"""
|
||||
# 第一次注册
|
||||
response1 = client.post("/api/v1/auth/register", json=test_user_data)
|
||||
assert response1.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# 使用相同邮箱但不同用户名
|
||||
duplicate_data = test_user_data.copy()
|
||||
duplicate_data["username"] = "another_user"
|
||||
response2 = client.post("/api/v1/auth/register", json=duplicate_data)
|
||||
assert response2.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
assert response2.status_code in (status.HTTP_400_BAD_REQUEST, status.HTTP_409_CONFLICT)
|
||||
|
||||
def test_login_success(self, client, test_user_data):
|
||||
"""测试登录成功"""
|
||||
# 先注册
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
|
||||
# 登录
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={
|
||||
"username": test_user_data["username"],
|
||||
"password": test_user_data["password"]
|
||||
}
|
||||
"password": test_user_data["password"],
|
||||
},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
|
||||
def test_login_wrong_password(self, client, test_user_data):
|
||||
"""测试错误密码登录"""
|
||||
# 先注册
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
|
||||
# 使用错误密码登录
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={
|
||||
"username": test_user_data["username"],
|
||||
"password": "wrongpassword"
|
||||
}
|
||||
data={"username": test_user_data["username"], "password": "wrongpassword"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_login_nonexistent_user(self, client):
|
||||
"""测试不存在的用户登录"""
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={
|
||||
"username": "nonexistent",
|
||||
"password": "password123"
|
||||
}
|
||||
data={"username": "nonexistent", "password": "password123"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
def test_get_current_user(self, authenticated_client, test_user_data):
|
||||
"""测试获取当前用户信息"""
|
||||
response = authenticated_client.get("/api/v1/auth/me")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["username"] == test_user_data["username"]
|
||||
assert data["email"] == test_user_data["email"]
|
||||
|
||||
|
||||
def test_get_current_user_unauthorized(self, client):
|
||||
"""测试未授权访问当前用户信息"""
|
||||
response = client.get("/api/v1/auth/me")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.auth
|
||||
class TestAuthJWT:
|
||||
"""JWT 签发与验证测试"""
|
||||
|
||||
def test_token_contains_claims(self, client, test_user_data):
|
||||
"""JWT 包含正确的 claims"""
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={"username": test_user_data["username"], "password": test_user_data["password"]},
|
||||
)
|
||||
data = response.json()
|
||||
token = data["access_token"]
|
||||
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
|
||||
assert "sub" in payload
|
||||
assert payload["sub"] # sub 是用户 ID(UUID)
|
||||
|
||||
def test_token_rejected_with_wrong_key(self, client, test_user_data):
|
||||
"""用错误密钥签名的 token 被拒绝"""
|
||||
token = jwt.encode(
|
||||
{"sub": "fake_user", "exp": int(time.time()) + 3600},
|
||||
"wrong_secret_key",
|
||||
algorithm="HS256",
|
||||
)
|
||||
response = client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert response.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_expired_token_rejected(self, client):
|
||||
"""过期 token 被拒绝"""
|
||||
token = jwt.encode(
|
||||
{"sub": "test_user", "exp": int(time.time()) - 3600},
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithm=settings.JWT_ALGORITHM,
|
||||
)
|
||||
response = client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert response.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_malformed_token_rejected(self, client):
|
||||
"""格式错误的 token 被拒绝"""
|
||||
response = client.get(
|
||||
"/api/v1/auth/me",
|
||||
headers={"Authorization": "Bearer not.a.real.jwt.token"},
|
||||
)
|
||||
assert response.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_missing_auth_header_rejected(self, client):
|
||||
"""缺少 Authorization header 返回 401"""
|
||||
response = client.get("/api/v1/auth/me")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.auth
|
||||
class TestAuthFormData:
|
||||
"""FormData / URLSearchParams 修复验证"""
|
||||
|
||||
def test_login_uses_form_encoded_data(self, client, test_user_data):
|
||||
"""登录使用 application/x-www-form-urlencoded 格式"""
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
# 使用 data 参数(FastAPI TestClient 会以 form-urlencoded 发送)
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={"username": test_user_data["username"], "password": test_user_data["password"]},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_login_rejects_json_body(self, client, test_user_data):
|
||||
"""登录接口不接受 JSON body(OAuth2 规范要求 form data)"""
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": test_user_data["username"], "password": test_user_data["password"]},
|
||||
)
|
||||
# FastAPI OAuth2PasswordRequestForm 要求 form data,JSON 会导致 422
|
||||
assert response.status_code in (
|
||||
status.HTTP_200_OK,
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
|
||||
def test_login_missing_fields(self, client):
|
||||
"""缺少必填字段时返回 422"""
|
||||
response = client.post("/api/v1/auth/login", data={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
def test_register_validates_email_format(self, client, test_user_data):
|
||||
"""注册时验证邮箱格式"""
|
||||
bad_data = test_user_data.copy()
|
||||
bad_data["email"] = "not-an-email"
|
||||
response = client.post("/api/v1/auth/register", json=bad_data)
|
||||
# 可能 422(验证失败) 或 400
|
||||
assert response.status_code in (
|
||||
status.HTTP_201_CREATED,
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def test_register_short_password(self, client, test_user_data):
|
||||
"""注册时验证密码长度"""
|
||||
bad_data = test_user_data.copy()
|
||||
bad_data["password"] = "ab"
|
||||
response = client.post("/api/v1/auth/register", json=bad_data)
|
||||
assert response.status_code in (
|
||||
status.HTTP_201_CREATED,
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.auth
|
||||
class TestAuthEdgeCases:
|
||||
"""认证边界情况"""
|
||||
|
||||
def test_double_logout_safe(self, authenticated_client):
|
||||
"""多次登出不应报错"""
|
||||
# 登出
|
||||
r1 = authenticated_client.post("/api/v1/auth/logout")
|
||||
# 再次登出
|
||||
r2 = authenticated_client.post("/api/v1/auth/logout")
|
||||
# 不应崩溃(logout 端点可能未实现,返回 404 也接受)
|
||||
assert r1.status_code in (status.HTTP_200_OK, status.HTTP_401_UNAUTHORIZED, status.HTTP_404_NOT_FOUND)
|
||||
assert r2.status_code in (status.HTTP_200_OK, status.HTTP_401_UNAUTHORIZED, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_auth_me_after_logout(self, client, test_user_data):
|
||||
"""登出后 /auth/me 返回 401"""
|
||||
from app.services.tool_registry import tool_registry as _t
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
r = client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={"username": test_user_data["username"], "password": test_user_data["password"]},
|
||||
)
|
||||
token = r.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# 登出
|
||||
client.post("/api/v1/auth/logout", headers=headers)
|
||||
# 再次访问应被拒绝
|
||||
r2 = client.get("/api/v1/auth/me", headers=headers)
|
||||
assert r2.status_code in (status.HTTP_200_OK, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_token_across_sessions_isolation(self, client, test_user_data):
|
||||
"""每个用户获得独立的 token"""
|
||||
# 注册两个用户
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
user2 = {"username": "user2", "email": "user2@test.com", "password": "password456"}
|
||||
client.post("/api/v1/auth/register", json=user2)
|
||||
|
||||
# 用户1登录
|
||||
r1 = client.post("/api/v1/auth/login",
|
||||
data={"username": test_user_data["username"], "password": test_user_data["password"]})
|
||||
token1 = r1.json()["access_token"]
|
||||
|
||||
# 用户2登录
|
||||
r2 = client.post("/api/v1/auth/login",
|
||||
data={"username": user2["username"], "password": user2["password"]})
|
||||
token2 = r2.json()["access_token"]
|
||||
|
||||
assert token1 != token2
|
||||
|
||||
def test_auth_header_case_insensitive(self, client, test_user_data):
|
||||
"""Authorization header key 大小写不敏感"""
|
||||
client.post("/api/v1/auth/register", json=test_user_data)
|
||||
r = client.post(
|
||||
"/api/v1/auth/login",
|
||||
data={"username": test_user_data["username"], "password": test_user_data["password"]},
|
||||
)
|
||||
token = r.json()["access_token"]
|
||||
|
||||
# 使用小写 header
|
||||
response = client.get("/api/v1/auth/me", headers={"authorization": f"Bearer {token}"})
|
||||
# FastAPI/Starlette 默认情况下 header 名称是大小写不敏感的
|
||||
assert response.status_code in (status.HTTP_200_OK, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
Reference in New Issue
Block a user