test_crawapi_http.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. """V3-M1A: shared crawapi HTTP base unit tests."""
  2. from __future__ import annotations
  3. import httpx
  4. import pytest
  5. from content_agent.errors import ContentAgentError, ErrorCode
  6. from content_agent.integrations.crawapi_http import (
  7. RateLimiter,
  8. is_rate_limit_business_error,
  9. post_crawapi_json,
  10. )
  11. class FakeHttpClient:
  12. def __init__(self, responses):
  13. self.responses = list(responses)
  14. self.requests = []
  15. def post(self, url, json, headers, timeout):
  16. self.requests.append({"url": url, "json": json})
  17. response = self.responses.pop(0)
  18. if isinstance(response, Exception):
  19. raise response
  20. return response
  21. def _response(status_code, data):
  22. return httpx.Response(
  23. status_code, json=data, request=httpx.Request("POST", "http://crawapi.test/x")
  24. )
  25. def _text_response(status_code, text):
  26. return httpx.Response(
  27. status_code,
  28. text=text,
  29. headers={"content-type": "text/plain"},
  30. request=httpx.Request("POST", "http://crawapi.test/x"),
  31. )
  32. def _post(responses, **kwargs):
  33. return post_crawapi_json(
  34. http_client=FakeHttpClient(responses),
  35. base_url="http://crawapi.test/",
  36. path="x",
  37. payload={},
  38. operation="probe",
  39. timeout_seconds=60.0,
  40. business_codes=kwargs.get("business_codes", set()),
  41. rate_limiter=kwargs.get("rate_limiter"),
  42. rate_limit_bucket=kwargs.get("rate_limit_bucket"),
  43. )
  44. def test_rate_limiter_waits_min_interval_between_same_bucket():
  45. clock = {"now": 0.0}
  46. sleeps: list[float] = []
  47. limiter = RateLimiter(
  48. min_interval_seconds=30.0,
  49. now_fn=lambda: clock["now"],
  50. sleep_fn=lambda s: (sleeps.append(s), clock.__setitem__("now", clock["now"] + s)),
  51. )
  52. limiter.wait("b")
  53. limiter.wait("b")
  54. assert sleeps == [30.0]
  55. def test_rate_limiter_supports_random_interval_between_same_bucket():
  56. clock = {"now": 0.0}
  57. sleeps: list[float] = []
  58. limiter = RateLimiter(
  59. min_interval_seconds=10.0,
  60. max_interval_seconds=12.0,
  61. now_fn=lambda: clock["now"],
  62. sleep_fn=lambda s: (sleeps.append(s), clock.__setitem__("now", clock["now"] + s)),
  63. random_fn=lambda lo, hi: (lo + hi) / 2,
  64. )
  65. limiter.wait("b")
  66. limiter.wait("b")
  67. assert sleeps == [11.0]
  68. def test_http_429_maps_to_platform_rate_limited():
  69. with pytest.raises(ContentAgentError) as exc:
  70. _post([_response(429, {"msg": "slow down"})])
  71. assert exc.value.error_code == ErrorCode.PLATFORM_RATE_LIMITED
  72. def test_message_token_maps_to_platform_rate_limited():
  73. with pytest.raises(ContentAgentError) as exc:
  74. _post([_response(200, {"code": 50000, "msg": "请求频繁"})])
  75. assert exc.value.error_code == ErrorCode.PLATFORM_RATE_LIMITED
  76. def test_bad_response_non_dict_raises_runtime_error():
  77. with pytest.raises(RuntimeError, match="bad_response"):
  78. _post([_response(200, ["not", "a", "dict"])])
  79. def test_http_error_includes_response_summary():
  80. with pytest.raises(RuntimeError) as exc:
  81. _post([_text_response(502, "Bad Gateway from crawapi")])
  82. message = str(exc.value)
  83. assert "HTTP 502" in message
  84. assert "Bad Gateway from crawapi" in message
  85. def test_business_error_includes_code_and_message():
  86. with pytest.raises(RuntimeError) as exc:
  87. _post([_response(200, {"code": 50001, "msg": "source url expired"})])
  88. message = str(exc.value)
  89. assert "business_error" in message
  90. assert "code=50001" in message
  91. assert "source url expired" in message
  92. def test_business_codes_param_classifies_rate_limit():
  93. assert is_rate_limit_business_error("30005", {}, business_codes={"30005"}) is True
  94. assert is_rate_limit_business_error("30005", {}, business_codes=set()) is False