Files
DadaoTask/backend/tests/test_clawbot.py
T
Steven c194609193
CI / backend (push) Canceled after 0s
fix(integrations): 回调地址漏了 /api/v1,生产上打过去是 405
生产把应用挂在域名根、路由带 /api/v1 前缀,request.base_url 里没有前缀,
拼出来的地址少了这一段;本地被 Vite 的代理掩盖了。改成从 base_url 截站点根、
前缀一律取 settings.api_prefix,并补一条断言把这个差异钉住。
2026-11-05 22:06:37 +08:00

208 lines
7.5 KiB
Python

"""微信(clawbot)接入:一条消息进来,变成任务或知识。
测的是**整条链路**:令牌 → 租户上下文 → 建事项 → 留痕 → 重复推送幂等。
webhook 不带登录态,所以这里故意不用 `auth()` 头,只有管理接口才带。
"""
from __future__ import annotations
from typing import Any
from httpx import AsyncClient
from .conftest import create_workspace, login, unwrap
async def _channel(client: AsyncClient, user: dict[str, Any], ws: dict[str, Any]) -> dict:
return unwrap(
await client.post(
f"/api/v1/workspaces/{ws['id']}/integrations",
json={"name": "我的微信"},
headers=user["headers"],
)
)
async def _post(client: AsyncClient, token: str, payload: dict[str, Any]):
return await client.post(f"/api/v1/integrations/clawbot/{token}", json=payload)
async def test_plain_message_becomes_task(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "微信接入")
channel = await _channel(client, user, ws)
resp = await _post(
client,
channel["token"],
{"msg_id": "wx-1", "from": "steven", "text": "给客户回电话\n顺便确认下报价"},
)
data = unwrap(resp)
assert data["action"] == "task"
assert data["duplicate"] is False
assert "给客户回电话" in data["reply"]
items = unwrap(
await client.get(f"/api/v1/workspaces/{ws['id']}/items", headers=user["headers"])
)["items"]
task = next(i for i in items if i["title"] == "给客户回电话")
assert task["kind"] == "task"
assert task["status"] == "inbox"
assert "顺便确认下报价" in task["body_md"]
async def test_knowledge_prefix_creates_draft_with_tags(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "微信知识")
channel = await _channel(client, user, ws)
data = unwrap(
await _post(
client,
channel["token"],
{"text": "知识:回滚前先停灰度 #发布 #回滚\n1. 停流量\n2. 回滚镜像"},
)
)
assert data["action"] == "knowledge"
docs = unwrap(
await client.get(
f"/api/v1/workspaces/{ws['id']}/knowledge?status=draft", headers=user["headers"]
)
)["items"]
doc = next(d for d in docs if d["title"] == "回滚前先停灰度")
assert doc["knowledge"]["status_hint"] == "draft"
assert doc["knowledge"]["doc_type"] == "note"
assert {t["name"] for t in doc["tags"]} == {"发布", "回滚"}
# 标签从标题摘出去以后,正文里不该再留 # 号
assert "#发布" not in doc["body_md"]
async def test_help_reply_creates_nothing(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "微信帮助")
channel = await _channel(client, user, ws)
data = unwrap(await _post(client, channel["token"], {"text": "帮助"}))
assert data["action"] == "help"
assert "记成任务" in data["reply"]
items = unwrap(
await client.get(f"/api/v1/workspaces/{ws['id']}/items", headers=user["headers"])
)["items"]
assert items == []
async def test_unknown_token_is_404(client: AsyncClient) -> None:
resp = await client.post("/api/v1/integrations/clawbot/nope", json={"text": "x"})
assert resp.status_code == 404
assert resp.json()["code"] == 9002
async def test_disabled_channel_refuses_messages(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "微信停用")
channel = await _channel(client, user, ws)
await client.patch(
f"/api/v1/workspaces/{ws['id']}/integrations/{channel['channel']['id']}",
json={"status": "disabled"},
headers=user["headers"],
)
resp = await _post(client, channel["token"], {"text": "这条不该进去"})
assert resp.status_code == 403
assert resp.json()["code"] == 9003
async def test_same_message_id_is_idempotent(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "微信重推")
channel = await _channel(client, user, ws)
payload = {"msg_id": "dup-1", "text": "只该有一条"}
first = unwrap(await _post(client, channel["token"], payload))
second = unwrap(await _post(client, channel["token"], payload))
assert second["duplicate"] is True
assert second["reply"] == first["reply"]
items = unwrap(
await client.get(f"/api/v1/workspaces/{ws['id']}/items", headers=user["headers"])
)["items"]
assert [i["title"] for i in items] == ["只该有一条"]
async def test_get_verify_echoes_challenge(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "微信校验")
channel = await _channel(client, user, ws)
resp = await client.get(f"/api/v1/integrations/clawbot/{channel['token']}?echostr=abc123")
assert resp.status_code == 200
assert resp.text == "abc123"
async def test_token_belongs_to_one_workspace(client: AsyncClient) -> None:
user = await login(client)
ws_a = await create_workspace(client, user, "A 区")
ws_b = await create_workspace(client, user, "B 区")
channel = await _channel(client, user, ws_a)
await _post(client, channel["token"], {"text": "只该进 A 区"})
items_a = unwrap(
await client.get(f"/api/v1/workspaces/{ws_a['id']}/items", headers=user["headers"])
)["items"]
items_b = unwrap(
await client.get(f"/api/v1/workspaces/{ws_b['id']}/items", headers=user["headers"])
)["items"]
assert [i["title"] for i in items_a] == ["只该进 A 区"]
assert items_b == []
async def test_rotate_invalidates_old_token(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "微信换令牌")
channel = await _channel(client, user, ws)
rotated = unwrap(
await client.post(
f"/api/v1/workspaces/{ws['id']}/integrations/{channel['channel']['id']}/rotate",
headers=user["headers"],
)
)
assert rotated["token"] != channel["token"]
old = await _post(client, channel["token"], {"text": "旧令牌"})
assert old.status_code == 404
new = unwrap(await _post(client, rotated["token"], {"text": "新令牌能用"}))
assert new["action"] == "task"
async def test_member_cannot_manage_channels(client: AsyncClient) -> None:
owner = await login(client)
ws = await create_workspace(client, owner, "微信权限")
member = await login(client)
code = unwrap(
await client.post(
f"/api/v1/workspaces/{ws['id']}/invitations",
json={"role": "member"},
headers=owner["headers"],
)
)
await client.post(
f"/api/v1/workspaces/{ws['id']}/join",
json={"code": code["code"]},
headers=member["headers"],
)
resp = await client.post(
f"/api/v1/workspaces/{ws['id']}/integrations",
json={"name": "偷加的通道"},
headers=member["headers"],
)
assert resp.status_code == 403
assert resp.json()["code"] in (3004, 3003)
async def test_webhook_url_carries_api_prefix(client: AsyncClient) -> None:
"""回调地址必须带 /api/v1:生产上应用挂在域名根,漏了前缀就是 405。"""
user = await login(client)
ws = await create_workspace(client, user, "微信地址")
channel = await _channel(client, user, ws)
assert channel["webhook_url"].endswith(f"/api/v1/integrations/clawbot/{channel['token']}")