import httpx import pytest from app.services.website_client import ( WebsiteError, _friendly_connection_error, _friendly_http_error, normalize_groups, ) # ——— normalize_groups ——— def test_normalize_groups_unwraps_sub2api_paginated_response(): groups = normalize_groups({ "code": 0, "message": "success", "data": { "items": [ {"id": "codex-free", "name": "codex-free", "rate_multiplier": 1}, {"id": "my-plus", "name": "my-plus", "rate_multiplier": "1.5"}, {"id": "deepseek", "name": "deepseek"}, ], "total": 3, "page": 1, }, }) assert [group["id"] for group in groups] == ["codex-free", "my-plus", "deepseek"] assert groups[0]["rate_multiplier"] == "1" assert groups[1]["rate_multiplier"] == "1.5" assert groups[2]["rate_multiplier"] is None def test_normalize_groups_unwraps_wrapped_list_response(): groups = normalize_groups({ "code": 0, "message": "success", "data": [ {"id": "default", "name": "Default", "rateMultiplier": "2.0"}, ], }) assert groups == [{ "id": "default", "name": "Default", "rate_multiplier": "2", "raw": {"id": "default", "name": "Default", "rateMultiplier": "2.0"}, }] def test_normalize_groups_unwraps_groups_key_response(): groups = normalize_groups({ "data": { "groups": [ {"group_id": "vip", "group_name": "VIP", "ratio": "0.75"}, ], }, }) assert groups[0]["id"] == "vip" assert groups[0]["name"] == "VIP" assert groups[0]["rate_multiplier"] == "0.75" def test_normalize_groups_keeps_string_list_compatibility(): groups = normalize_groups(["free", "paid"]) assert [group["id"] for group in groups] == ["free", "paid"] assert groups[0]["raw"] == {"id": "free", "name": "free"} def test_normalize_groups_keeps_plain_dict_mapping_compatibility(): groups = normalize_groups({ "free": {"id": "free", "name": "Free", "rate_multiplier": "1.00"}, "paid": {"id": "paid", "name": "Paid"}, }) assert [group["id"] for group in groups] == ["free", "paid"] assert groups[0]["rate_multiplier"] == "1" # ——— _get_account_ids / account_exists ——— def test_get_account_ids_flat_list(): from app.services.website_client import Sub2ApiWebsiteClient ids = Sub2ApiWebsiteClient._unwrap_list([ {"id": 1, "name": "a"}, {"id": 2, "name": "b"}, ]) assert ids == [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}] def test_get_account_ids_top_level_items(): from app.services.website_client import Sub2ApiWebsiteClient ids = Sub2ApiWebsiteClient._unwrap_list({"items": [ {"id": "k1"}, {"id": "k2"}, ]}) assert ids == [{"id": "k1"}, {"id": "k2"}] def test_get_account_ids_nested_data_items(): from app.services.website_client import Sub2ApiWebsiteClient ids = Sub2ApiWebsiteClient._unwrap_list({"data": {"items": [ {"id": "a1", "name": "Alpha"}, {"id": "a2", "name": "Beta"}, ]}}) assert ids == [{"id": "a1", "name": "Alpha"}, {"id": "a2", "name": "Beta"}] def test_get_account_ids_nested_data_empty(): from app.services.website_client import Sub2ApiWebsiteClient ids = Sub2ApiWebsiteClient._unwrap_list({"data": {"items": []}}) assert ids == [] def test_get_account_ids_unexpected_format(): from app.services.website_client import Sub2ApiWebsiteClient ids = Sub2ApiWebsiteClient._unwrap_list({"error": "not found"}) assert ids is None # ——— 友好错误提示 ——— def _make_response(status_code: int, path: str = "/groups") -> httpx.Response: """创建模拟的 httpx.Response 用于错误测试。""" req = httpx.Request("GET", f"http://target.local/api/v1{path}") resp = httpx.Response(status_code, request=req) return resp def test_friendly_http_401(): resp = _make_response(401) exc = httpx.HTTPStatusError("401", request=resp.request, response=resp) msg = _friendly_http_error(exc) assert "认证失败" in msg assert "API Key" in msg assert "http://" not in msg def test_friendly_http_403(): resp = _make_response(403) exc = httpx.HTTPStatusError("403", request=resp.request, response=resp) msg = _friendly_http_error(exc) assert "权限不足" in msg def test_friendly_http_404(): resp = _make_response(404, path="/wrong-path") exc = httpx.HTTPStatusError("404", request=resp.request, response=resp) msg = _friendly_http_error(exc) assert "接口不存在" in msg assert "/wrong-path" in msg # 不包含完整 URL / MDN 链接 assert "http://" not in msg assert "MDN" not in msg def test_friendly_http_500(): resp = _make_response(502) exc = httpx.HTTPStatusError("502", request=resp.request, response=resp) msg = _friendly_http_error(exc) assert "服务异常" in msg def test_friendly_connect_error(): exc = httpx.ConnectError("Connection refused") msg = _friendly_connection_error(exc) assert "无法连接" in msg def test_friendly_timeout_error(): exc = httpx.TimeoutException("Timed out") msg = _friendly_connection_error(exc) assert "请求超时" in msg # ——— get_groups fallback(通过 mock httpx client 触发真实 _request 错误转换) ——— def _mock_httpx_request(status_code: int, path: str = "/groups"): """返回一个 mock 的 httpx.Client.request,直接抛 HTTPStatusError。""" def request(self, method, url, **kwargs): resp = _make_response(status_code, path=path) raise httpx.HTTPStatusError(f"{status_code} {path}", request=resp.request, response=resp) return request def test_get_groups_401_returns_friendly_auth_error(monkeypatch): from app.services.website_client import Sub2ApiWebsiteClient monkeypatch.setattr(httpx.Client, "request", _mock_httpx_request(401)) client = Sub2ApiWebsiteClient("http://target.local", "api/v1", "api_key", {"key": "bad"}) with pytest.raises(WebsiteError) as excinfo: client.get_groups("/groups") msg = str(excinfo.value) assert "认证失败" in msg assert "http://" not in msg assert "MDN" not in msg def test_get_groups_404_fallback_succeeds(monkeypatch): from app.services.website_client import Sub2ApiWebsiteClient call_count = 0 def request_fallback(self, method, url, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: resp = _make_response(404) raise httpx.HTTPStatusError("404", request=resp.request, response=resp) req = httpx.Request("GET", url) return httpx.Response(200, json={"data": [{"id": "default", "name": "Default", "rate_multiplier": "1"}]}, request=req) monkeypatch.setattr(httpx.Client, "request", request_fallback) client = Sub2ApiWebsiteClient("http://target.local", "api/v1", "api_key", {"key": "ok"}) groups = client.get_groups("/groups") assert len(groups) == 1 assert groups[0]["id"] == "default" assert call_count == 2 def test_get_groups_all_404_no_raw_url(monkeypatch): from app.services.website_client import Sub2ApiWebsiteClient monkeypatch.setattr(httpx.Client, "request", _mock_httpx_request(404)) client = Sub2ApiWebsiteClient("http://target.local", "api/v1", "api_key", {"key": "ok"}) with pytest.raises(WebsiteError) as excinfo: client.get_groups("/groups") msg = str(excinfo.value) assert "接口不存在" in msg assert "http://" not in msg assert "MDN" not in msg