test_base_mailbox.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import json
  2. import pytest
  3. from core.base_mailbox import BaseMailbox, MailboxAccount, create_mailbox
  4. from core.cfmail import DEFAULT_CFMAIL_MANAGER, CfMailMailbox, CfmailAccount
  5. class FakeResponse:
  6. def __init__(self, payload, status_code: int = 200): # type: ignore[no-untyped-def]
  7. self._payload = payload
  8. self.status_code = status_code
  9. self.content = b"payload"
  10. self.text = json.dumps(payload, ensure_ascii=False)
  11. def json(self): # type: ignore[no-untyped-def]
  12. return self._payload
  13. class DummyCfmailManager:
  14. def __init__(self) -> None:
  15. self.account = CfmailAccount(
  16. name="demo",
  17. worker_domain="email-api.example.com",
  18. email_domain="mail.example.com",
  19. admin_password="secret",
  20. )
  21. self.successes = 0
  22. self.failures: list[str] = []
  23. def reload_if_needed(self) -> bool:
  24. return False
  25. def select_account(self, profile_name=None): # type: ignore[no-untyped-def]
  26. del profile_name
  27. return self.account
  28. def record_success(self, account_name: str) -> None:
  29. assert account_name == self.account.name
  30. self.successes += 1
  31. def record_failure(self, account_name: str, reason: str = "") -> None:
  32. assert account_name == self.account.name
  33. self.failures.append(reason)
  34. def account_names(self) -> str:
  35. return self.account.name
  36. class PartialMailbox(BaseMailbox):
  37. def get_email(self) -> MailboxAccount:
  38. return MailboxAccount(email="demo@example.com")
  39. class DummyMailbox(BaseMailbox):
  40. def get_email(self) -> MailboxAccount:
  41. return MailboxAccount(email="demo@example.com", account_id="token")
  42. def wait_for_code(
  43. self,
  44. account: MailboxAccount,
  45. keyword: str = "",
  46. timeout: int = 120,
  47. before_ids: set[str] | None = None,
  48. ) -> str:
  49. del account, keyword, timeout, before_ids
  50. return "123456"
  51. def get_current_ids(self, account: MailboxAccount) -> set[str]:
  52. del account
  53. return {"msg-1"}
  54. def test_base_mailbox_requires_all_abstract_methods() -> None:
  55. with pytest.raises(TypeError):
  56. PartialMailbox()
  57. def test_mailbox_account_and_base_interface_contract() -> None:
  58. mailbox = DummyMailbox()
  59. account = mailbox.get_email()
  60. assert account == MailboxAccount(email="demo@example.com", account_id="token", extra={})
  61. assert mailbox.get_current_ids(account) == {"msg-1"}
  62. assert mailbox.wait_for_code(account) == "123456"
  63. def test_create_mailbox_only_supports_cfmail() -> None:
  64. mailbox = create_mailbox("cfmail")
  65. assert isinstance(mailbox, CfMailMailbox)
  66. assert mailbox.manager is DEFAULT_CFMAIL_MANAGER
  67. with pytest.raises(ValueError, match="Unsupported mailbox provider"):
  68. create_mailbox("mailtm")
  69. def test_cfmail_get_email_retries_transient_transport_errors(monkeypatch: pytest.MonkeyPatch) -> None:
  70. manager = DummyCfmailManager()
  71. mailbox = CfMailMailbox(manager=manager, proxy="socks5://127.0.0.1:18043")
  72. calls = {"count": 0}
  73. seen_proxies: list[object] = []
  74. def fake_post(url, **kwargs): # type: ignore[no-untyped-def]
  75. del url
  76. calls["count"] += 1
  77. seen_proxies.append(kwargs.get("proxies"))
  78. if calls["count"] < 3:
  79. raise RuntimeError(
  80. "Failed to perform, curl: (35) TLS connect error: "
  81. "error:00000000:OPENSSL_internal:invalid library"
  82. )
  83. return FakeResponse(
  84. {
  85. "address": "ocdemo@mail.example.com",
  86. "jwt": "jwt-demo",
  87. }
  88. )
  89. monkeypatch.setattr("core.cfmail.cffi_requests.post", fake_post)
  90. monkeypatch.setattr("core.cfmail.time.sleep", lambda *_args, **_kwargs: None)
  91. account = mailbox.get_email()
  92. assert calls["count"] == 3
  93. assert seen_proxies == [None, None, None]
  94. assert account.email == "ocdemo@mail.example.com"
  95. assert account.account_id == "jwt-demo"
  96. assert manager.successes == 1
  97. assert manager.failures == []
  98. def test_cfmail_get_email_raises_after_exhausting_transient_retries(monkeypatch: pytest.MonkeyPatch) -> None:
  99. manager = DummyCfmailManager()
  100. mailbox = CfMailMailbox(manager=manager, proxy="socks5://127.0.0.1:18043")
  101. calls = {"count": 0}
  102. def fake_post(url, **kwargs): # type: ignore[no-untyped-def]
  103. del url, kwargs
  104. calls["count"] += 1
  105. raise RuntimeError("Failed to perform, curl: (28) Connection timed out after 15000 milliseconds.")
  106. monkeypatch.setattr("core.cfmail.cffi_requests.post", fake_post)
  107. monkeypatch.setattr("core.cfmail.time.sleep", lambda *_args, **_kwargs: None)
  108. with pytest.raises(RuntimeError, match="curl: \\(28\\)"):
  109. mailbox.get_email()
  110. assert calls["count"] == 3
  111. assert len(manager.failures) == 1
  112. assert "new_address exception" in manager.failures[0]
  113. def test_cfmail_get_email_retries_retryable_http_statuses(monkeypatch: pytest.MonkeyPatch) -> None:
  114. manager = DummyCfmailManager()
  115. mailbox = CfMailMailbox(manager=manager)
  116. calls = {"count": 0}
  117. def fake_post(url, **kwargs): # type: ignore[no-untyped-def]
  118. del url, kwargs
  119. calls["count"] += 1
  120. if calls["count"] < 3:
  121. return FakeResponse({"error": "temporary upstream failure"}, status_code=503)
  122. return FakeResponse({"address": "ocrun@mail.example.com", "jwt": "jwt-run"})
  123. monkeypatch.setattr("core.cfmail.cffi_requests.post", fake_post)
  124. monkeypatch.setattr("core.cfmail.time.sleep", lambda *_args, **_kwargs: None)
  125. account = mailbox.get_email()
  126. assert calls["count"] == 3
  127. assert account.email == "ocrun@mail.example.com"
  128. assert account.account_id == "jwt-run"
  129. assert manager.successes == 1
  130. def test_cfmail_get_email_includes_http_400_body_snippet(monkeypatch: pytest.MonkeyPatch) -> None:
  131. manager = DummyCfmailManager()
  132. mailbox = CfMailMailbox(manager=manager)
  133. def fake_post(url, **kwargs): # type: ignore[no-untyped-def]
  134. del url, kwargs
  135. return FakeResponse({"error": "D1 database full"}, status_code=400)
  136. monkeypatch.setattr("core.cfmail.cffi_requests.post", fake_post)
  137. with pytest.raises(RuntimeError, match=r"HTTP 400.*D1 database full"):
  138. mailbox.get_email()
  139. assert len(manager.failures) == 1
  140. assert "HTTP 400" in manager.failures[0]
  141. def test_cfmail_wait_for_code_uses_expanded_window_and_records_diagnostics(monkeypatch: pytest.MonkeyPatch) -> None:
  142. manager = DummyCfmailManager()
  143. mailbox = CfMailMailbox(manager=manager, proxy="socks5://127.0.0.1:18043")
  144. monkeypatch.setattr("core.cfmail.CFMAIL_WAIT_PROGRESS_CALLBACK", None)
  145. account = MailboxAccount(
  146. email="ocdemo@mail.example.com",
  147. account_id="jwt-demo",
  148. extra={"api_base": "https://email-api.example.com", "config_name": "demo"},
  149. )
  150. captured_limits: list[int] = []
  151. class WaitResponse:
  152. def __init__(self, payload):
  153. self.status_code = 200
  154. self.content = b"{}"
  155. self._payload = payload
  156. def json(self):
  157. return self._payload
  158. responses = [
  159. WaitResponse({"results": [{"id": "old-1", "address": account.email, "raw": "stale"}]}),
  160. WaitResponse({"results": [{"id": "new-1", "address": account.email, "raw": "Your ChatGPT code is 654321"}]}),
  161. ]
  162. def fake_request_with_retry(**kwargs): # type: ignore[no-untyped-def]
  163. captured_limits.append(int(kwargs["params"]["limit"]))
  164. return responses.pop(0)
  165. tick = iter([100.0, 100.0, 100.2, 100.5, 101.0, 101.0, 101.2, 101.3, 101.4])
  166. monkeypatch.setattr(mailbox, "_request_with_retry", fake_request_with_retry)
  167. monkeypatch.setattr("core.cfmail.CFMAIL_WAIT_ABORT_PREDICATE", None)
  168. monkeypatch.setattr("core.cfmail.time.sleep", lambda *_a, **_k: None)
  169. monkeypatch.setattr("core.cfmail.time.time", lambda: next(tick))
  170. current_ids = mailbox.get_current_ids(account)
  171. code = mailbox.wait_for_code(account, timeout=30, before_ids=current_ids)
  172. assert current_ids == {"old-1"}
  173. assert code == "654321"
  174. assert captured_limits == [30, 30]
  175. assert mailbox.last_wait_diagnostics["first_message_seen_at"] == 100.5
  176. assert mailbox.last_wait_diagnostics["matched_message_at"] == 101.0
  177. assert mailbox.last_wait_diagnostics["poll_count"] == 1
  178. def test_cfmail_mailbox_uses_direct_egress_even_when_register_proxy_is_configured() -> None:
  179. mailbox = CfMailMailbox(manager=DummyCfmailManager(), proxy="socks5://127.0.0.1:18043")
  180. assert mailbox.proxies is None
  181. def test_cfmail_wait_for_code_aborts_when_rotation_predicate_requests_it(monkeypatch: pytest.MonkeyPatch) -> None:
  182. manager = DummyCfmailManager()
  183. mailbox = CfMailMailbox(manager=manager)
  184. monkeypatch.setattr("core.cfmail.CFMAIL_WAIT_PROGRESS_CALLBACK", None)
  185. account = MailboxAccount(
  186. email="ocdemo@mail.example.com",
  187. account_id="jwt-demo",
  188. extra={"api_base": "https://email-api.example.com", "config_name": "demo"},
  189. )
  190. monkeypatch.setattr("core.cfmail.CFMAIL_WAIT_ABORT_PREDICATE", lambda _account: True)
  191. code = mailbox.wait_for_code(account, timeout=30, before_ids=set())
  192. assert code == ""
  193. assert mailbox.last_wait_diagnostics["aborted"] is True
  194. assert mailbox.last_wait_diagnostics["abort_reason"] == "rotation_or_stoploss"
  195. def test_cfmail_wait_for_code_ignores_messages_older_than_not_before_timestamp(monkeypatch: pytest.MonkeyPatch) -> None:
  196. manager = DummyCfmailManager()
  197. mailbox = CfMailMailbox(manager=manager)
  198. monkeypatch.setattr("core.cfmail.CFMAIL_WAIT_PROGRESS_CALLBACK", None)
  199. account = MailboxAccount(
  200. email="ocdemo@mail.example.com",
  201. account_id="jwt-demo",
  202. extra={"api_base": "https://email-api.example.com", "config_name": "demo"},
  203. )
  204. class WaitResponse:
  205. def __init__(self, payload):
  206. self.status_code = 200
  207. self.content = b"{}"
  208. self._payload = payload
  209. def json(self):
  210. return self._payload
  211. responses = [
  212. WaitResponse(
  213. {
  214. "results": [
  215. {
  216. "id": "old-msg",
  217. "address": account.email,
  218. "raw": "Your ChatGPT code is 111111",
  219. "createdAt": "2026-03-29T05:00:00Z",
  220. },
  221. {
  222. "id": "new-msg",
  223. "address": account.email,
  224. "raw": "Your ChatGPT code is 222222",
  225. "createdAt": "2026-03-29T05:00:30Z",
  226. },
  227. ]
  228. }
  229. )
  230. ]
  231. monkeypatch.setattr(mailbox, "_request_with_retry", lambda **kwargs: responses.pop(0))
  232. monkeypatch.setattr("core.cfmail.CFMAIL_WAIT_ABORT_PREDICATE", None)
  233. monkeypatch.setattr("core.cfmail.time.sleep", lambda *_a, **_k: None)
  234. monkeypatch.setattr("core.cfmail.time.time", lambda: 1743224431.0)
  235. code = mailbox.wait_for_code(
  236. account,
  237. timeout=30,
  238. before_ids=set(),
  239. not_before_timestamp=1774760430.0,
  240. )
  241. assert code == "222222"