1. 更新能力注册测试用例,修正空能力默认动作断言 2. 调整渠道API测试的错误状态码校验逻辑,新增停止不存在渠道的测试 3. 重构飞书动作测试的状态校验逻辑,使用枚举替代字符串判断 4. 新增渠道状态条目管理的集成测试用例
204 lines
6.9 KiB
Python
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)
|