From f419f57f63413db807eb1219c6f6836f7f059fa8 Mon Sep 17 00:00:00 2001 From: Kris <2893855659@qq.com> Date: Thu, 14 May 2026 02:09:58 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=B8=A0=E9=81=93=E7=9B=B8=E5=85=B3=E5=8D=95=E5=85=83?= =?UTF-8?q?=E4=B8=8E=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 更新能力注册测试用例,修正空能力默认动作断言 2. 调整渠道API测试的错误状态码校验逻辑,新增停止不存在渠道的测试 3. 重构飞书动作测试的状态校验逻辑,使用枚举替代字符串判断 4. 新增渠道状态条目管理的集成测试用例 --- .../api/test_channel_state_entry.py | 203 ++++++++++++++++++ .../integration/api/test_channels_router.py | 19 +- .../test/unit/channels/core/test_actions.py | 9 +- .../channels/core/test_message_actions_v2.py | 13 +- 4 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 backend/test/integration/api/test_channel_state_entry.py diff --git a/backend/test/integration/api/test_channel_state_entry.py b/backend/test/integration/api/test_channel_state_entry.py new file mode 100644 index 00000000..ccd7bb51 --- /dev/null +++ b/backend/test/integration/api/test_channel_state_entry.py @@ -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) diff --git a/backend/test/integration/api/test_channels_router.py b/backend/test/integration/api/test_channels_router.py index e69cf9ea..709c12c7 100644 --- a/backend/test/integration/api/test_channels_router.py +++ b/backend/test/integration/api/test_channels_router.py @@ -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 \ No newline at end of file + assert response.status_code == 404 diff --git a/backend/test/unit/channels/core/test_actions.py b/backend/test/unit/channels/core/test_actions.py index 7955d4a5..f6cfe21b 100644 --- a/backend/test/unit/channels/core/test_actions.py +++ b/backend/test/unit/channels/core/test_actions.py @@ -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" diff --git a/backend/test/unit/channels/core/test_message_actions_v2.py b/backend/test/unit/channels/core/test_message_actions_v2.py index 9dff3408..649be3ad 100644 --- a/backend/test/unit/channels/core/test_message_actions_v2.py +++ b/backend/test/unit/channels/core/test_message_actions_v2.py @@ -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