test: 修复并完善渠道相关单元与集成测试
1. 更新能力注册测试用例,修正空能力默认动作断言 2. 调整渠道API测试的错误状态码校验逻辑,新增停止不存在渠道的测试 3. 重构飞书动作测试的状态校验逻辑,使用枚举替代字符串判断 4. 新增渠道状态条目管理的集成测试用例
This commit is contained in:
parent
29dac24600
commit
f419f57f63
203
backend/test/integration/api/test_channel_state_entry.py
Normal file
203
backend/test/integration/api/test_channel_state_entry.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user