Files
group_fqcd_jr/tests/unit/api/test_risk_stream_negotiation.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

104 lines
3.6 KiB
Python

"""风控 SSE 端点的内容协商与鉴权时序(`docs/25` P3 #24)。
`POST /api/v1/risk/daily-report/stream` 此前既不校验 `Accept`,又把鉴权留在
async generator 内部 —— 后者更隐蔽:`StreamingResponse` 已经返回、响应头已经发出,
`403` 只能变成"200 + 半截流"。所以这里同时断言两件事:
1. 显式只接受 `application/json` → `406 SSE_NOT_ACCEPTABLE`,且响应体是统一 JSON 错误;
2. 无权限时即使 `Accept` 也非法,仍先得到 `403 AGENT_PERMISSION_DENIED`(顺序与
`docs/05` §6.4 一致:鉴权先行,防止用状态码差异做探测)。
"""
from collections.abc import AsyncIterator
from typing import Any
import pytest
from fastapi.testclient import TestClient
from app.api.dependencies.auth import build_request_context
from app.api.dependencies.database import get_session
from app.core.contracts import RequestContext
from app.core.errors import AgentPermissionDeniedError
from app.main import create_app
STREAM_PATH = "/api/v1/risk/daily-report/stream"
async def resolve_context() -> RequestContext:
return RequestContext(
user_id="1",
trace_id="trace-1",
permissions=("risk:alert:read",),
data_scope="all",
)
class StubReportService:
"""替身:`authorize` 通过、流立即收尾,避免测试连接挂住。"""
def __init__(self, session: Any) -> None:
self.session = session
async def authorize(self, _context: RequestContext) -> None:
return None
async def stream(
self,
_context: RequestContext,
_now: Any,
) -> AsyncIterator[dict[str, Any]]:
yield {"type": "done", "report": {}}
class DenyingReportService(StubReportService):
async def authorize(self, _context: RequestContext) -> None:
raise AgentPermissionDeniedError("缺少 risk:alert:read 权限")
def client_with(
monkeypatch: pytest.MonkeyPatch,
service_class: type[StubReportService],
) -> TestClient:
application = create_app()
application.dependency_overrides[build_request_context] = resolve_context
application.dependency_overrides[get_session] = lambda: None
monkeypatch.setattr("app.api.controllers.risk.RiskDailyReportService", service_class)
return TestClient(application)
@pytest.mark.parametrize("accept", [None, "", "*/*", "text/*", "text/event-stream"])
def test_absent_or_wildcard_accept_is_allowed(
monkeypatch: pytest.MonkeyPatch, accept: str | None
) -> None:
headers = {} if accept is None else {"Accept": accept}
with client_with(monkeypatch, StubReportService) as client:
response = client.post(STREAM_PATH, json={}, headers=headers)
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/event-stream")
def test_json_only_accept_is_rejected_before_streaming(
monkeypatch: pytest.MonkeyPatch,
) -> None:
with client_with(monkeypatch, StubReportService) as client:
response = client.post(
STREAM_PATH, json={}, headers={"Accept": "application/json"}
)
assert response.status_code == 406
assert response.json()["error"]["code"] == "SSE_NOT_ACCEPTABLE"
assert response.headers["content-type"].startswith("application/json")
def test_authorization_precedes_accept_negotiation(
monkeypatch: pytest.MonkeyPatch,
) -> None:
with client_with(monkeypatch, DenyingReportService) as client:
response = client.post(
STREAM_PATH, json={}, headers={"Accept": "application/json"}
)
assert response.status_code == 403
assert response.json()["error"]["code"] == "AGENT_PERMISSION_DENIED"