"""用户面(神识阁数据源)端点测试:BYOK 管理与用量查询,经回声桩客户端端到端。""" from collections.abc import AsyncGenerator import pytest from httpx import ASGITransport, AsyncClient from app.kernel import Kernel from app.main import app from app.services.event_store import DbEventStore from app.db.session import AsyncSessionLocal from app.kernel import KernelEventLog from app.shenshi_client.client import EchoShenshi @pytest.fixture async def client( monkeypatch: pytest.MonkeyPatch, auth_headers: dict[str, str] ) -> AsyncGenerator[AsyncClient, None]: """手工装配 Kernel(回声桩客户端)到 app.state(测试不经 lifespan,与既有做法一致)。""" app.state.kernel = Kernel( shenshi=EchoShenshi(), session_factory=AsyncSessionLocal, event_log=KernelEventLog(DbEventStore(AsyncSessionLocal)), timeout_seconds=10.0, ) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test", headers=auth_headers) as c: yield c app.state.kernel = None @pytest.mark.asyncio async def test_byok_register_list_revoke(client: AsyncClient) -> None: payload = { "name": "自带 DeepSeek", "type": "openai_compatible", "base_url": "https://api.deepseek.com", "api_key": "sk-very-secret-value", "model": "deepseek-v4-flash", } created = await client.post("/api/v1/me/byok-keys", json=payload) assert created.status_code == 201 body = created.json() # 脱敏:响应不得含明文密钥 assert "sk-very-secret-value" not in body assert "****" in body.get("api_key_mask", "") key_id = body["id"] listed = await client.get("/api/v1/me/byok-keys") assert listed.status_code == 200 keys = listed.json()["keys"] assert [k["id"] for k in keys] == [key_id] assert all("sk-very-secret-value" not in str(k) for k in keys) revoked = await client.delete(f"/api/v1/me/byok-keys/{key_id}") assert revoked.status_code == 204 after = await client.get("/api/v1/me/byok-keys") assert after.json()["keys"] == [] @pytest.mark.asyncio async def test_byok_register_validation(client: AsyncClient) -> None: bad = await client.post( "/api/v1/me/byok-keys", json={"name": "x", "type": "unknown_type", "base_url": "https://x", "api_key": "k" * 8, "model": "m"}, ) # 类型枚举由神识侧裁决;回声桩直通,这里校验必填字段约束 missing = await client.post("/api/v1/me/byok-keys", json={"name": "x"}) assert missing.status_code == 422 @pytest.mark.asyncio async def test_my_usage_shape(client: AsyncClient) -> None: resp = await client.get("/api/v1/me/usage") assert resp.status_code == 200 body = resp.json() assert "rows" in body assert isinstance(body["rows"], list) @pytest.mark.asyncio async def test_me_requires_auth() -> None: transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as c: resp = await c.get("/api/v1/me/byok-keys") assert resp.status_code in (401, 503) # 未携带凭证被拒(JWT 未配置时 fail-closed)