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)