test: 修复并完善渠道相关单元与集成测试

1. 更新能力注册测试用例,修正空能力默认动作断言
2. 调整渠道API测试的错误状态码校验逻辑,新增停止不存在渠道的测试
3. 重构飞书动作测试的状态校验逻辑,使用枚举替代字符串判断
4. 新增渠道状态条目管理的集成测试用例
This commit is contained in:
Kris 2026-05-14 02:09:58 +08:00
parent 29dac24600
commit f419f57f63
4 changed files with 224 additions and 20 deletions

View File

@ -0,0 +1,203 @@
from __future__ import annotations
import asyncio
import pytest
from httpx import AsyncClient
TEST_CHANNEL = "telegram"
TEST_NS = "pytest"
TEST_KEY_PREFIX = "test_state_entry"
def _make_key(suffix: str = "") -> str:
return f"{TEST_KEY_PREFIX}_{suffix}" if suffix else TEST_KEY_PREFIX
def _url(channel: str = TEST_CHANNEL, ns: str = TEST_NS, key: str = "") -> str:
return f"/api/channels/{channel}/state/{ns}/{key or _make_key()}"
@pytest.mark.integration
class TestChannelStateEntryWrite:
async def test_write_and_read_back(self, test_client: AsyncClient, admin_headers: dict):
key = _make_key("readback")
put_resp = await test_client.put(
_url(key=key),
json={"value": {"a": 1}},
headers=admin_headers,
)
assert put_resp.status_code in (200, 201)
put_data = put_resp.json()
assert put_data["code"] == 0
get_resp = await test_client.get(_url(key=key), headers=admin_headers)
assert get_resp.status_code == 200
get_data = get_resp.json()
assert get_data["data"]["value"] == {"a": 1}
async def test_write_with_ttl(self, test_client: AsyncClient, admin_headers: dict):
key = _make_key("ttl")
put_resp = await test_client.put(
_url(key=key),
json={"value": "temp", "ttl_seconds": 3},
headers=admin_headers,
)
assert put_resp.status_code in (200, 201)
get_resp = await test_client.get(_url(key=key), headers=admin_headers)
assert get_resp.status_code == 200
assert get_resp.json()["data"]["value"] == "temp"
await asyncio.sleep(4)
get_expired = await test_client.get(_url(key=key), headers=admin_headers)
assert get_expired.status_code == 404
async def test_overwrite_update(self, test_client: AsyncClient, admin_headers: dict):
key = _make_key("overwrite")
put1 = await test_client.put(
_url(key=key),
json={"value": 1},
headers=admin_headers,
)
assert put1.status_code in (200, 201)
put2 = await test_client.put(
_url(key=key),
json={"value": 2},
headers=admin_headers,
)
assert put2.status_code == 200
get_resp = await test_client.get(_url(key=key), headers=admin_headers)
assert get_resp.json()["data"]["value"] == 2
async def test_first_write_returns_201(self, test_client: AsyncClient, admin_headers: dict):
key = _make_key("created")
put_resp = await test_client.put(
_url(key=key),
json={"value": "new"},
headers=admin_headers,
)
assert put_resp.status_code == 201
assert "已创建" in put_resp.json()["message"]
put2 = await test_client.put(
_url(key=key),
json={"value": "updated"},
headers=admin_headers,
)
assert put2.status_code == 200
assert "已更新" in put2.json()["message"]
@pytest.mark.integration
class TestChannelStateEntryValidation:
async def test_missing_value(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.put(
_url(key=_make_key("noval")),
json={"ttl_seconds": 60},
headers=admin_headers,
)
assert response.status_code == 422
async def test_ttl_seconds_invalid_type(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.put(
_url(key=_make_key("badttl")),
json={"value": "x", "ttl_seconds": "abc"},
headers=admin_headers,
)
assert response.status_code == 422
async def test_ttl_seconds_negative(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.put(
_url(key=_make_key("negttl")),
json={"value": "x", "ttl_seconds": -1},
headers=admin_headers,
)
assert response.status_code == 422
async def test_ttl_seconds_zero(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.put(
_url(key=_make_key("zerottl")),
json={"value": "x", "ttl_seconds": 0},
headers=admin_headers,
)
assert response.status_code == 422
@pytest.mark.integration
class TestChannelStateEntryPathValidation:
async def test_channel_id_too_long(self, test_client: AsyncClient, admin_headers: dict):
long_id = "a" * 33
response = await test_client.put(
f"/api/channels/{long_id}/state/{TEST_NS}/some_key",
json={"value": "x"},
headers=admin_headers,
)
assert response.status_code == 422
async def test_namespace_too_long(self, test_client: AsyncClient, admin_headers: dict):
long_ns = "a" * 65
response = await test_client.put(
f"/api/channels/{TEST_CHANNEL}/state/{long_ns}/some_key",
json={"value": "x"},
headers=admin_headers,
)
assert response.status_code == 422
async def test_key_too_long(self, test_client: AsyncClient, admin_headers: dict):
long_key = "a" * 129
response = await test_client.put(
f"/api/channels/{TEST_CHANNEL}/state/{TEST_NS}/{long_key}",
json={"value": "x"},
headers=admin_headers,
)
assert response.status_code == 422
@pytest.mark.integration
class TestChannelStateEntryErrors:
async def test_nonexistent_channel(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.put(
"/api/channels/ghost-channel/state/default/test_key",
json={"value": "x"},
headers=admin_headers,
)
assert response.status_code == 404
async def test_non_admin_rejected(self, test_client: AsyncClient, standard_user: dict):
response = await test_client.put(
_url(key=_make_key("noadmin")),
json={"value": "x"},
headers=standard_user["headers"],
)
assert response.status_code in (401, 403)
@pytest.mark.integration
class TestChannelStateEntryConcurrency:
async def test_concurrent_writes_same_key(self, test_client: AsyncClient, admin_headers: dict):
key = _make_key("concurrent")
async def put_value(v: int):
return await test_client.put(
_url(key=key),
json={"value": v},
headers=admin_headers,
)
tasks = [put_value(i) for i in range(10)]
responses = await asyncio.gather(*tasks)
for i, resp in enumerate(responses):
assert resp.status_code in (200, 201), f"Request {i} failed: {resp.status_code} {resp.text}"
get_resp = await test_client.get(_url(key=key), headers=admin_headers)
assert get_resp.status_code == 200
final_value = get_resp.json()["data"]["value"]
assert final_value in range(10)

View File

@ -26,15 +26,20 @@ class TestChannelsStatusAPI:
class TestChannelStartStop:
async def test_start_nonexistent_channel(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.post("/api/channels/nonexistent/start", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert data["code"] == -1
assert response.status_code == 404
async def test_stop_not_running_channel(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.post("/api/channels/telegram/stop", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert data["code"] == 409
assert "not running" in data["message"]
async def test_stop_nonexistent_channel(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.post("/api/channels/nonexistent/stop", headers=admin_headers)
assert response.status_code == 404
data = response.json()
assert "not registered" in data["detail"]
@pytest.mark.integration
@ -45,9 +50,7 @@ class TestChannelConfig:
json={"enabled": True},
headers=admin_headers,
)
assert response.status_code == 200
data = response.json()
assert data["code"] == -1
assert response.status_code == 404
@pytest.mark.integration
@ -74,6 +77,4 @@ class TestChannelStats:
class TestChannelTestConnection:
async def test_test_channel_nonexistent(self, test_client: AsyncClient, admin_headers: dict):
response = await test_client.post("/api/channels/nonexistent/test", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert data["code"] == -1
assert response.status_code == 404

View File

@ -219,13 +219,13 @@ class TestActionRegistryExplicitCapabilities:
class TestChannelCapabilitiesSupportedActions:
def test_empty_caps_has_no_actions(self):
def test_empty_caps_has_baseline_actions(self):
caps = ChannelCapabilities()
assert caps.supported_actions() == []
assert caps.supported_actions() == ["send", "reply"]
def test_simple_text_has_no_actions(self):
def test_simple_text_has_baseline_actions(self):
actions = CAPS_SIMPLE_TEXT.supported_actions()
assert actions == []
assert actions == ["send", "reply"]
def test_full_featured_has_expected_actions(self):
actions = CAPS_FULL_FEATURED.supported_actions()
@ -277,7 +277,6 @@ class TestMessageActionEnumExpanded:
assert MessageAction.PERMISSIONS == "permissions"
def test_thread_actions(self):
assert MessageAction.THREAD_CREATE == "thread_create"
assert MessageAction.THREAD_LIST == "thread_list"
assert MessageAction.THREAD_REPLY == "thread_reply"

View File

@ -5,6 +5,7 @@ from yuxi.channels.adapters.discord.message_actions import DISCORD_MESSAGE_ACTIO
from yuxi.channels.adapters.slack.message_actions import SLACK_MESSAGE_ACTIONS
from yuxi.channels.adapters.whatsapp.message_actions import WHATSAPP_MESSAGE_ACTIONS
from yuxi.channels.adapters.feishu.message_actions import FEISHU_MESSAGE_ACTIONS
from yuxi.channels.message_actions import ActionStatus
class TestMessageActionsSchema:
@ -86,14 +87,14 @@ class TestFeishuActions:
def test_feishu_actions_have_required_fields(self):
assert len(FEISHU_MESSAGE_ACTIONS) >= 20
for action_name, action_info in FEISHU_MESSAGE_ACTIONS.items():
assert "status" in action_info, f"Missing status in {action_name}"
assert action_info["status"] in ("implemented", "planned", "unsupported"), \
f"Invalid status '{action_info['status']}' in {action_name}"
assert "description" in action_info, f"Missing description in {action_name}"
assert "api" in action_info, f"Missing api in {action_name}"
assert hasattr(action_info, "status"), f"Missing status in {action_name}"
assert action_info.status in (ActionStatus.SUPPORTED, ActionStatus.PARTIAL, ActionStatus.UNSUPPORTED), \
f"Invalid status '{action_info.status}' in {action_name}"
assert hasattr(action_info, "description"), f"Missing description in {action_name}"
assert hasattr(action_info, "api"), f"Missing api in {action_name}"
def test_feishu_implemented_actions_exist(self):
implemented = [k for k, v in FEISHU_MESSAGE_ACTIONS.items() if v["status"] == "implemented"]
implemented = [k for k, v in FEISHU_MESSAGE_ACTIONS.items() if v.status == ActionStatus.SUPPORTED]
assert "send" in implemented
assert "edit" in implemented
assert "delete" in implemented