ForcePilot/backend/test/integration/api/test_channel_state_entry.py
Kris f419f57f63 test: 修复并完善渠道相关单元与集成测试
1. 更新能力注册测试用例,修正空能力默认动作断言
2. 调整渠道API测试的错误状态码校验逻辑,新增停止不存在渠道的测试
3. 重构飞书动作测试的状态校验逻辑,使用枚举替代字符串判断
4. 新增渠道状态条目管理的集成测试用例
2026-05-14 02:09:58 +08:00

204 lines
6.9 KiB
Python

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)