Files
2026-07-13 13:12:33 +08:00

187 lines
5.9 KiB
Python

from __future__ import annotations
import json
import httpx
import pytest
from opensquilla.search.providers.bocha import BochaSearchProvider
from opensquilla.search.registry import get_provider_spec
from opensquilla.search.types import SearchProviderError
@pytest.mark.asyncio
async def test_bocha_search_posts_summary_request_and_maps_results() -> None:
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(
200,
json={
"code": 200,
"msg": "success",
"log_id": "log-bocha-1",
"data": {
"webPages": {
"value": [
{
"id": "https://example.cn/a",
"name": "Bocha result",
"url": "https://example.cn/a",
"snippet": "Snippet text",
"summary": "Summary text",
"siteName": "Example CN",
"displayUrl": "example.cn/a",
"datePublished": "2026-03-10T23:21:00+08:00",
"dateLastCrawled": "2026-03-11T08:00:00+08:00",
}
]
}
},
},
)
provider = BochaSearchProvider(
api_key="dummy-bocha-key",
transport=httpx.MockTransport(handler),
)
results = await provider.search("OpenSquilla", max_results=3, recency="year")
assert len(requests) == 1
assert requests[0].method == "POST"
assert str(requests[0].url) == "https://api.bochaai.com/v1/web-search"
assert requests[0].headers["Authorization"] == "Bearer dummy-bocha-key"
assert requests[0].headers["Content-Type"] == "application/json"
body = json.loads(requests[0].content)
assert body == {
"query": "OpenSquilla",
"count": 3,
"summary": True,
"freshness": "oneYear",
}
result = results[0]
assert result.provider == "bocha"
assert result.source == "bocha"
assert result.title == "Bocha result"
assert result.url == "https://example.cn/a"
assert result.snippet == "Snippet text"
assert result.content == "Summary text"
assert result.published_at == "2026-03-10T23:21:00+08:00"
assert result.raw_metadata["log_id"] == "log-bocha-1"
assert result.raw_metadata["siteName"] == "Example CN"
assert result.raw_metadata["displayUrl"] == "example.cn/a"
@pytest.mark.asyncio
async def test_bocha_search_clamps_count_to_provider_limit() -> None:
requests: list[httpx.Request] = []
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(200, json={"code": 200, "data": {"webPages": {"value": []}}})
provider = BochaSearchProvider(
api_key="dummy-bocha-key",
transport=httpx.MockTransport(handler),
)
await provider.search("OpenSquilla", max_results=40)
body = json.loads(requests[0].content)
assert body["count"] == 20
@pytest.mark.asyncio
async def test_bocha_missing_api_key_raises_auth_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("BOCHA_SEARCH_API_KEY", raising=False)
provider = BochaSearchProvider()
with pytest.raises(SearchProviderError) as exc_info:
await provider.search("OpenSquilla")
assert exc_info.value.provider == "bocha"
assert exc_info.value.kind == "auth"
assert exc_info.value.retryable is False
assert str(exc_info.value) == "Bocha API key not set"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("status_code", "kind", "retryable"),
[
(401, "auth", False),
(403, "auth", False),
(429, "rate_limit", True),
(500, "http", True),
],
)
async def test_bocha_http_errors_are_classified(
status_code: int,
kind: str,
retryable: bool,
) -> None:
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code, json={"error": "nope"})
provider = BochaSearchProvider(
api_key="dummy-bocha-key",
transport=httpx.MockTransport(handler),
)
with pytest.raises(SearchProviderError) as exc_info:
await provider.search("OpenSquilla")
assert exc_info.value.provider == "bocha"
assert exc_info.value.kind == kind
assert exc_info.value.retryable is retryable
assert exc_info.value.status_code == status_code
@pytest.mark.asyncio
@pytest.mark.parametrize(
("api_code", "kind", "retryable", "status_code"),
[
("401", "auth", False, 401),
("429", "rate_limit", True, 429),
("500", "http", True, 500),
],
)
async def test_bocha_api_error_codes_are_classified(
api_code: str,
kind: str,
retryable: bool,
status_code: int,
) -> None:
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"code": api_code, "msg": "bocha error"})
provider = BochaSearchProvider(
api_key="dummy-bocha-key",
transport=httpx.MockTransport(handler),
)
with pytest.raises(SearchProviderError) as exc_info:
await provider.search("OpenSquilla")
assert exc_info.value.provider == "bocha"
assert exc_info.value.kind == kind
assert exc_info.value.retryable is retryable
assert exc_info.value.status_code == status_code
assert str(exc_info.value) == "bocha error"
def test_bocha_provider_spec_is_runtime_supported_after_import() -> None:
import opensquilla.search.providers.bocha # noqa: F401
spec = get_provider_spec("bocha")
assert spec.runtime_supported is True
assert spec.requires_api_key is True
assert spec.env_key == "BOCHA_SEARCH_API_KEY"
assert spec.capabilities == frozenset({"web", "freshness", "content"})