auto-sync: 2026-06-05 18:45:24

This commit is contained in:
cfdaily
2026-06-05 18:45:24 +08:00
parent 76945a47d6
commit e23aa1fed2
+375
View File
@@ -0,0 +1,375 @@
"""Mail API 集成测试 — 覆盖全部端点 + 防御逻辑 A1-A10"""
import json
import os
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from src.main import app
pytestmark = pytest.mark.integration
# 有效 Agent(与 mail_routes fallback 一致)
VALID_AGENTS = [
"zhangfei-dev", "guanyu-dev", "zhaoyun-data",
"jiangwei-infra", "pangtong-fujunshi", "simayi-challenger",
]
FROM_AGENT = "zhangfei-dev"
TO_AGENT = "simayi-challenger"
OTHER_AGENT = "pangtong-fujunshi"
@pytest.fixture
def mail_env(tmp_path):
"""创建临时 Mail 环境,确保 BLACKBOARD_ROOT 隔离"""
mail_root = tmp_path / "mail_root"
mail_root.mkdir()
os.environ["BLACKBOARD_ROOT"] = str(mail_root)
yield mail_root
del os.environ["BLACKBOARD_ROOT"]
@pytest.fixture
def client():
return TestClient(app)
def _send(client, from_a: str = FROM_AGENT, to_a: str = TO_AGENT,
title: str = "Test Mail", text: str = "Hello",
**kwargs) -> dict:
"""发送邮件的便捷方法"""
body = {"from": from_a, "to": to_a, "title": title, "text": text}
body.update(kwargs)
resp = client.post("/api/mail", json=body)
return resp
# ===================================================================
# TestMailList
# ===================================================================
class TestMailList:
def test_list_returns_all(self, client, mail_env):
_send(client, title="Mail A")
_send(client, from_a=TO_AGENT, to_a=FROM_AGENT, title="Mail B")
resp = client.get("/api/mail")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 2
def test_filter_by_from_agent(self, client, mail_env):
_send(client, title="From ZF")
_send(client, from_a=OTHER_AGENT, to_a=TO_AGENT, title="From PT")
resp = client.get("/api/mail", params={"from_agent": FROM_AGENT})
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
assert data["mails"][0]["from"] == FROM_AGENT
def test_filter_by_to_agent(self, client, mail_env):
_send(client, title="To SMY")
_send(client, from_a=TO_AGENT, to_a=OTHER_AGENT, title="To PT")
resp = client.get("/api/mail", params={"to_agent": TO_AGENT})
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
assert data["mails"][0]["to"] == TO_AGENT
def test_filter_unread(self, client, mail_env):
r = _send(client, title="Unread")
mail_id = r.json()["mail_id"]
# 标记已读
client.patch(f"/api/mail/{mail_id}", json={"is_read": True})
# 再发一封未读
_send(client, from_a=OTHER_AGENT, to_a=FROM_AGENT, title="Still unread")
resp = client.get("/api/mail", params={"unread": True})
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
def test_limit_parameter(self, client, mail_env):
for i in range(5):
_send(client, title=f"Mail {i}")
resp = client.get("/api/mail", params={"limit": 2})
assert resp.status_code == 200
assert resp.json()["total"] == 2
# ===================================================================
# TestMailAgents
# ===================================================================
class TestMailAgents:
def test_agents_list(self, client, mail_env):
_send(client, title="A")
_send(client, from_a=OTHER_AGENT, to_a=TO_AGENT, title="B")
resp = client.get("/api/mail/agents/list")
assert resp.status_code == 200
agents = resp.json()["agents"]
assert FROM_AGENT in agents
assert TO_AGENT in agents
assert OTHER_AGENT in agents
# ===================================================================
# TestMailSummary
# ===================================================================
class TestMailSummary:
def test_summary_fields(self, client, mail_env):
_send(client, title="Info", type="inform")
_send(client, title="Req", type="request")
resp = client.get("/api/mail/summary")
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 2
assert data["unread"] == 2
assert data["by_type"]["inform"] == 1
assert data["by_type"]["request"] == 1
# ===================================================================
# TestMailDetail
# ===================================================================
class TestMailDetail:
def test_get_detail(self, client, mail_env):
r = _send(client, title="Detail Test", text="Body content")
mail_id = r.json()["mail_id"]
resp = client.get(f"/api/mail/{mail_id}")
assert resp.status_code == 200
data = resp.json()
assert data["title"] == "Detail Test"
assert data["description"] == "Body content"
assert data["from"] == FROM_AGENT
assert data["to"] == TO_AGENT
assert "comments" in data
def test_detail_404(self, client, mail_env):
resp = client.get("/api/mail/nonexistent-mail-id")
assert resp.status_code == 404
# ===================================================================
# TestSendMail — A1-A10 防御逻辑
# ===================================================================
class TestSendMail:
# --- 防御逻辑 ---
def test_A1_from_missing(self, client, mail_env):
"""A1: from 必填 → 400"""
resp = client.post("/api/mail", json={
"to": TO_AGENT, "title": "T", "text": "Hi",
})
assert resp.status_code == 400
assert "from" in resp.json()["detail"]
def test_A9_from_invalid(self, client, mail_env):
"""A9: from 不是有效 Agent → 400"""
resp = client.post("/api/mail", json={
"from": "unknown-agent", "to": TO_AGENT, "title": "T", "text": "Hi",
})
assert resp.status_code == 400
assert "不是有效的 Agent" in resp.json()["detail"]
def test_A2_to_missing_non_reply(self, client, mail_env):
"""A2: 非回复时 to 缺失 → 400"""
resp = client.post("/api/mail", json={
"from": FROM_AGENT, "title": "T", "text": "Hi",
})
assert resp.status_code == 400
assert "to" in resp.json()["detail"]
def test_A3_from_equals_to(self, client, mail_env):
"""A3: from == to 防自环 → 400"""
resp = client.post("/api/mail", json={
"from": FROM_AGENT, "to": FROM_AGENT, "title": "T", "text": "Self",
})
assert resp.status_code == 400
assert "不能给自己发邮件" in resp.json()["detail"]
def test_A4_to_invalid(self, client, mail_env):
"""A4: to 不是有效 Agent → 400"""
resp = client.post("/api/mail", json={
"from": FROM_AGENT, "to": "nobody", "title": "T", "text": "Hi",
})
assert resp.status_code == 400
assert "不是有效的 Agent" in resp.json()["detail"]
def test_A10_text_empty(self, client, mail_env):
"""A10: 正文为空 → 400"""
resp = client.post("/api/mail", json={
"from": FROM_AGENT, "to": TO_AGENT, "title": "T", "text": " ",
})
assert resp.status_code == 400
assert "正文" in resp.json()["detail"]
def test_A10_description_empty(self, client, mail_env):
"""A10: description 字段为空也触发 → 400"""
resp = client.post("/api/mail", json={
"from": FROM_AGENT, "to": TO_AGENT, "title": "T",
})
assert resp.status_code == 400
# --- 正常发送 ---
def test_send_inform(self, client, mail_env):
"""正常发送 inform → 200"""
resp = _send(client, type="inform")
assert resp.status_code == 200
data = resp.json()
assert data["ok"]
assert data["mail_id"]
# 验证列表可见
mails = client.get("/api/mail").json()["mails"]
assert len(mails) == 1
assert mails[0]["type"] == "inform"
def test_send_request(self, client, mail_env):
"""正常发送 request → 200"""
resp = _send(client, type="request")
assert resp.status_code == 200
mails = client.get("/api/mail").json()["mails"]
assert mails[0]["type"] == "request"
# --- 回复逻辑 ---
def test_A5_reply_to_nonexistent(self, client, mail_env):
"""A5: in_reply_to 不存在 → 400"""
resp = _send(client, in_reply_to="mail-99999")
assert resp.status_code == 400
assert "不存在" in resp.json()["detail"]
def test_A6_A7_auto_correct_to(self, client, mail_env):
"""A6/A7: 回复时 to 被自动纠正为原发件者 → 200 + auto_corrected"""
# simayi → zhangfei
r = _send(client, from_a=TO_AGENT, to_a=FROM_AGENT,
title="Original", text="Original body")
orig_id = r.json()["mail_id"]
# zhangfei 回复 simayi,但故意传错 to
resp = _send(client, from_a=FROM_AGENT, to_a=OTHER_AGENT,
title="Reply", text="Reply body",
in_reply_to=orig_id)
assert resp.status_code == 200
data = resp.json()
assert data["ok"]
assert "auto_corrected" in data
assert data["auto_corrected"]["field"] == "to"
assert data["auto_corrected"]["original"] == OTHER_AGENT
assert data["auto_corrected"]["corrected"] == TO_AGENT
def test_A8_non_party_reply_rejected(self, client, mail_env):
"""A8: 非邮件双方回复 → 400"""
# zhangfei → simayi
r = _send(client, title="Private", text="Secret")
orig_id = r.json()["mail_id"]
# pangtong(第三方)尝试回复
resp = _send(client, from_a=OTHER_AGENT, to_a=FROM_AGENT,
title="Intrude", text="I shouldn't be here",
in_reply_to=orig_id)
assert resp.status_code == 400
assert "发送者或接收者" in resp.json()["detail"]
def test_reply_inherits_conversation_id(self, client, mail_env):
"""回复时 conversation_id 继承"""
r = _send(client, from_a=TO_AGENT, to_a=FROM_AGENT,
title="Conv Start", text="Start",
conversation_id="conv-test-123")
orig_id = r.json()["mail_id"]
resp = _send(client, from_a=FROM_AGENT, to_a=TO_AGENT,
title="Conv Reply", text="Reply",
in_reply_to=orig_id)
assert resp.status_code == 200
reply_id = resp.json()["mail_id"]
# 验证回复邮件继承了 conversation_id
detail = client.get(f"/api/mail/{reply_id}").json()
assert detail["conversation_id"] == "conv-test-123"
def test_reply_type_defaults_inform(self, client, mail_env):
"""回复时 type 默认 inform"""
r = _send(client, from_a=TO_AGENT, to_a=FROM_AGENT,
title="Orig", text="Hi", type="request")
orig_id = r.json()["mail_id"]
# 不传 type,应默认 inform
resp = client.post("/api/mail", json={
"from": FROM_AGENT, "to": TO_AGENT,
"title": "Re: Orig", "text": "Reply",
"in_reply_to": orig_id,
})
assert resp.status_code == 200
reply_id = resp.json()["mail_id"]
detail = client.get(f"/api/mail/{reply_id}").json()
assert detail["type"] == "inform"
# ===================================================================
# TestDeleteMail
# ===================================================================
class TestDeleteMail:
def test_delete_by_prefix(self, client, mail_env):
_send(client, title="[BULK] Alert 1")
_send(client, title="[BULK] Alert 2")
_send(client, title="Keep this")
resp = client.delete("/api/mail", params={"prefix": "[BULK]"})
assert resp.status_code == 200
data = resp.json()
assert data["ok"]
assert data["deleted_count"] == 2
# 只有 1 封保留
mails = client.get("/api/mail").json()["mails"]
assert len(mails) == 1
assert mails[0]["title"] == "Keep this"
def test_delete_prefix_missing(self, client, mail_env):
resp = client.delete("/api/mail")
assert resp.status_code == 400
def test_delete_no_match(self, client, mail_env):
_send(client, title="Keep")
resp = client.delete("/api/mail", params={"prefix": "NONEXISTENT"})
assert resp.status_code == 200
assert resp.json()["deleted_count"] == 0
# ===================================================================
# TestUpdateMail
# ===================================================================
class TestUpdateMail:
def test_mark_read(self, client, mail_env):
r = _send(client, title="Read me")
mail_id = r.json()["mail_id"]
resp = client.patch(f"/api/mail/{mail_id}", json={"is_read": True})
assert resp.status_code == 200
assert resp.json()["ok"]
# 验证已读
detail = client.get(f"/api/mail/{mail_id}").json()
assert detail["is_read"] is True
def test_mark_executed(self, client, mail_env):
r = _send(client, title="Do me")
mail_id = r.json()["mail_id"]
resp = client.patch(f"/api/mail/{mail_id}", json={"mark_executed": True})
assert resp.status_code == 200
assert resp.json()["ok"]
# 验证已读 + status=done
detail = client.get(f"/api/mail/{mail_id}").json()
assert detail["is_read"] is True
assert detail["status"] == "done"
def test_update_404(self, client, mail_env):
resp = client.patch("/api/mail/no-such-mail", json={"is_read": True})
assert resp.status_code == 404