mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-06-01 05:31:01 +08:00
feat(email): add configurable resend limits for non-OpenAI sender emails
This commit is contained in:
155
tests/check_otp_timing.py
Normal file
155
tests/check_otp_timing.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
离线验证 TempmailService 的 OTP 时间锚点过滤行为。
|
||||
|
||||
场景 1:
|
||||
- 30 秒内先后收到两封邮件
|
||||
- 在两封邮件之间设置新的 otp_sent_at
|
||||
- 期望过滤第一封,命中第二封
|
||||
|
||||
场景 2:
|
||||
- 第二封邮件已经入箱后才刷新 otp_sent_at
|
||||
- 期望复现严格时间过滤导致第二封也被排除的窗口
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import src.services.tempmail as tempmail_module
|
||||
from src.services.tempmail import TempmailService
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Scenario:
|
||||
name: str
|
||||
anchor_offset_seconds: int
|
||||
expected_code: Optional[str]
|
||||
expected_message: str
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload: Dict[str, Any], status_code: int = 200):
|
||||
self._payload = payload
|
||||
self.status_code = status_code
|
||||
|
||||
def json(self) -> Dict[str, Any]:
|
||||
return self._payload
|
||||
|
||||
|
||||
class FakeHTTPClient:
|
||||
def __init__(self, payload: Dict[str, Any]):
|
||||
self.payload = payload
|
||||
self.calls: List[Dict[str, Any]] = []
|
||||
|
||||
def get(self, url: str, **kwargs: Any) -> FakeResponse:
|
||||
self.calls.append({"url": url, "kwargs": kwargs})
|
||||
return FakeResponse(self.payload)
|
||||
|
||||
|
||||
class FakeClock:
|
||||
def __init__(self, start: float):
|
||||
self.current = float(start)
|
||||
|
||||
def time(self) -> float:
|
||||
return self.current
|
||||
|
||||
def sleep(self, seconds: float) -> None:
|
||||
self.current += float(seconds)
|
||||
|
||||
|
||||
def build_inbox_payload(base_timestamp: int) -> Dict[str, Any]:
|
||||
return {
|
||||
"emails": [
|
||||
{
|
||||
"id": "mail-1",
|
||||
"received_at": base_timestamp + 10,
|
||||
"from": "noreply@openai.com",
|
||||
"subject": "First OTP",
|
||||
"body": "111111",
|
||||
},
|
||||
{
|
||||
"id": "mail-2",
|
||||
"received_at": base_timestamp + 20,
|
||||
"from": "noreply@openai.com",
|
||||
"subject": "Second OTP",
|
||||
"body": "222222",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def run_scenario(scenario: Scenario) -> Dict[str, Any]:
|
||||
base_timestamp = 1_700_000_000
|
||||
service = TempmailService({"base_url": "https://api.tempmail.test"})
|
||||
service._email_cache["tester@example.com"] = {"token": "token-1"}
|
||||
service.http_client = FakeHTTPClient(build_inbox_payload(base_timestamp))
|
||||
|
||||
fake_clock = FakeClock(start=base_timestamp + scenario.anchor_offset_seconds)
|
||||
anchor_timestamp = fake_clock.time()
|
||||
original_time = tempmail_module.time.time
|
||||
original_sleep = tempmail_module.time.sleep
|
||||
|
||||
try:
|
||||
tempmail_module.time.time = fake_clock.time
|
||||
tempmail_module.time.sleep = fake_clock.sleep
|
||||
code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
otp_sent_at=anchor_timestamp,
|
||||
)
|
||||
finally:
|
||||
tempmail_module.time.time = original_time
|
||||
tempmail_module.time.sleep = original_sleep
|
||||
|
||||
passed = code == scenario.expected_code
|
||||
return {
|
||||
"name": scenario.name,
|
||||
"anchor_timestamp": anchor_timestamp,
|
||||
"code": code,
|
||||
"passed": passed,
|
||||
"http_calls": len(service.http_client.calls),
|
||||
"message": scenario.expected_message,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
logging.getLogger("src.services.tempmail").setLevel(logging.ERROR)
|
||||
|
||||
scenarios = [
|
||||
Scenario(
|
||||
name="anchor_between_two_emails",
|
||||
anchor_offset_seconds=15,
|
||||
expected_code="222222",
|
||||
expected_message="新锚点位于两封邮件之间,第一封应被过滤,第二封应被命中。",
|
||||
),
|
||||
Scenario(
|
||||
name="anchor_set_after_second_email",
|
||||
anchor_offset_seconds=21,
|
||||
expected_code=None,
|
||||
expected_message="锚点晚于第二封邮件时,严格大于过滤会把第二封也排除,复现登录阶段的竞态窗口。",
|
||||
),
|
||||
]
|
||||
|
||||
print("Tempmail OTP timing check")
|
||||
print("=========================")
|
||||
|
||||
failed = False
|
||||
for scenario in scenarios:
|
||||
result = run_scenario(scenario)
|
||||
status = "PASS" if result["passed"] else "FAIL"
|
||||
print(f"{status} {result['name']}")
|
||||
print(f" anchor_timestamp={result['anchor_timestamp']}")
|
||||
print(f" returned_code={result['code']}")
|
||||
print(f" inbox_polls={result['http_calls']}")
|
||||
print(f" note={result['message']}")
|
||||
if not result["passed"]:
|
||||
failed = True
|
||||
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
103
tests/probe_tempmail.py
Normal file
103
tests/probe_tempmail.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tempmail.lol API 探针。
|
||||
|
||||
用途:
|
||||
1. 创建测试收件箱或复用现有 token。
|
||||
2. 拉取 /inbox 原始 JSON 并原样打印。
|
||||
3. 检查邮件对象里是否存在 received_at/date 等时间字段。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, Iterable
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
DEFAULT_BASE_URL = "https://api.tempmail.lol/v2"
|
||||
TIME_FIELDS = ("received_at", "date", "created_at", "createdAt", "timestamp")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="抓取 Tempmail.lol 收件箱原始 JSON")
|
||||
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Tempmail API 基础地址")
|
||||
parser.add_argument("--token", help="已有 inbox token;未提供时自动创建新邮箱")
|
||||
parser.add_argument("--poll-count", type=int, default=1, help="轮询次数")
|
||||
parser.add_argument("--poll-interval", type=float, default=3.0, help="轮询间隔秒数")
|
||||
parser.add_argument("--timeout", type=float, default=20.0, help="HTTP 超时时间")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def dump_json(title: str, payload: Dict[str, Any]) -> None:
|
||||
print(f"\n===== {title} =====")
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def summarize_time_fields(emails: Iterable[Dict[str, Any]]) -> None:
|
||||
for index, message in enumerate(emails, start=1):
|
||||
present_fields = {name: message.get(name) for name in TIME_FIELDS if name in message}
|
||||
print(f"email[{index}] 时间字段: {json.dumps(present_fields, ensure_ascii=False, default=str)}")
|
||||
|
||||
|
||||
def create_inbox(client: httpx.Client, base_url: str) -> Dict[str, Any]:
|
||||
response = client.post(
|
||||
f"{base_url}/inbox/create",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={},
|
||||
)
|
||||
print(f"CREATE_STATUS {response.status_code}")
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
dump_json("CREATE_RESPONSE", payload)
|
||||
return payload
|
||||
|
||||
|
||||
def fetch_inbox(client: httpx.Client, base_url: str, token: str) -> Dict[str, Any]:
|
||||
response = client.get(
|
||||
f"{base_url}/inbox",
|
||||
params={"token": token},
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
print(f"INBOX_STATUS {response.status_code}")
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
dump_json("INBOX_RESPONSE", payload)
|
||||
emails = payload.get("emails", []) if isinstance(payload, dict) else []
|
||||
if isinstance(emails, list):
|
||||
summarize_time_fields([mail for mail in emails if isinstance(mail, dict)])
|
||||
else:
|
||||
print(f"emails 字段不是列表: {type(emails).__name__}")
|
||||
return payload
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
with httpx.Client(timeout=args.timeout) as client:
|
||||
token = args.token
|
||||
if not token:
|
||||
inbox = create_inbox(client, args.base_url)
|
||||
token = str(inbox.get("token", "")).strip()
|
||||
address = str(inbox.get("address", "")).strip()
|
||||
print(f"ADDRESS {address}")
|
||||
print(f"TOKEN {token}")
|
||||
|
||||
if not token:
|
||||
print("未拿到 token,无法继续拉取 inbox", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
for attempt in range(1, args.poll_count + 1):
|
||||
print(f"\n----- poll {attempt}/{args.poll_count} -----")
|
||||
fetch_inbox(client, args.base_url, token)
|
||||
if attempt < args.poll_count:
|
||||
time.sleep(args.poll_interval)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
45
tests/test_mail_openai_detection.py
Normal file
45
tests/test_mail_openai_detection.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from src.services import EmailServiceType
|
||||
from src.services.base import BaseEmailService
|
||||
|
||||
|
||||
class DummyEmailService(BaseEmailService):
|
||||
def __init__(self):
|
||||
super().__init__(EmailServiceType.TEMPMAIL, name="dummy")
|
||||
|
||||
def create_email(self, config=None):
|
||||
return {"email": "dummy@example.com", "service_id": "dummy"}
|
||||
|
||||
def get_verification_code(self, **kwargs):
|
||||
return None
|
||||
|
||||
def list_emails(self, **kwargs):
|
||||
return []
|
||||
|
||||
def delete_email(self, email_id: str) -> bool:
|
||||
return True
|
||||
|
||||
def check_health(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def test_is_openai_candidate_message_supports_sender_and_content_paths():
|
||||
service = DummyEmailService()
|
||||
|
||||
assert service._is_openai_candidate_message("noreply@openai.com", "hello") is True
|
||||
assert service._is_openai_candidate_message("notice@example.com", "Your OpenAI verification code") is True
|
||||
assert service._is_openai_candidate_message("notice@example.com", "newsletter") is False
|
||||
|
||||
|
||||
def test_batch_has_openai_sender_only_checks_sender_fields():
|
||||
service = DummyEmailService()
|
||||
batch = [
|
||||
{"from": "notice@example.com", "body": "openai mentioned in content"},
|
||||
{"from": "alerts@example.com", "body": "still not sender"},
|
||||
]
|
||||
|
||||
assert service._batch_has_openai_sender(batch, lambda item: item.get("from")) is False
|
||||
assert service._batch_has_openai_sender(
|
||||
batch + [{"from": "otp@tm1.openai.com", "body": "code"}],
|
||||
lambda item: item.get("from"),
|
||||
) is True
|
||||
|
||||
127
tests/test_outlook_service_config_and_health.py
Normal file
127
tests/test_outlook_service_config_and_health.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from src.services import EmailServiceType
|
||||
from src.services.outlook.base import EmailMessage, ProviderType
|
||||
from src.services.outlook.email_parser import EmailParser
|
||||
from src.services.outlook.health_checker import HealthChecker
|
||||
from src.services.outlook.service import OutlookService
|
||||
from src.services.base import OTPNoOpenAISenderEmailServiceError
|
||||
from src.web.routes import registration as registration_routes
|
||||
|
||||
|
||||
def test_health_checker_is_scoped_by_account_email():
|
||||
checker = HealthChecker(failure_threshold=1, disable_duration=120)
|
||||
|
||||
checker.record_failure(ProviderType.IMAP_OLD, "boom", account_email="a@example.com")
|
||||
|
||||
assert checker.is_available(ProviderType.IMAP_OLD, "b@example.com") is True
|
||||
assert checker.is_available(ProviderType.IMAP_OLD, "a@example.com") is False
|
||||
|
||||
account_a_status = checker.get_all_health_status("a@example.com")
|
||||
assert account_a_status[ProviderType.IMAP_OLD.value]["status"] == "disabled"
|
||||
|
||||
|
||||
def test_email_parser_respects_recipient_match_switch():
|
||||
parser = EmailParser()
|
||||
mail = EmailMessage(
|
||||
id="m1",
|
||||
subject="Your verification code",
|
||||
sender="noreply@openai.com",
|
||||
recipients=["other@example.com"],
|
||||
body="Your code is 123456",
|
||||
received_timestamp=123,
|
||||
)
|
||||
|
||||
assert parser.is_openai_verification_email(
|
||||
mail,
|
||||
target_email="target@example.com",
|
||||
require_recipient_match=True,
|
||||
) is False
|
||||
assert parser.is_openai_verification_email(
|
||||
mail,
|
||||
target_email="target@example.com",
|
||||
require_recipient_match=False,
|
||||
) is True
|
||||
|
||||
assert parser.find_verification_code_in_emails(
|
||||
[mail],
|
||||
target_email="target@example.com",
|
||||
require_recipient_match=True,
|
||||
) is None
|
||||
assert parser.find_verification_code_in_emails(
|
||||
[mail],
|
||||
target_email="target@example.com",
|
||||
require_recipient_match=False,
|
||||
) == "123456"
|
||||
|
||||
|
||||
def test_normalize_outlook_config_inherits_global_settings(monkeypatch):
|
||||
class DummySettings:
|
||||
outlook_provider_priority = ["imap_old", "graph_api"]
|
||||
outlook_health_failure_threshold = 4
|
||||
outlook_health_disable_duration = 90
|
||||
outlook_require_recipient_match = True
|
||||
|
||||
monkeypatch.setattr(registration_routes, "get_settings", lambda: DummySettings())
|
||||
|
||||
normalized = registration_routes._normalize_email_service_config(
|
||||
EmailServiceType.OUTLOOK,
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"require_recipient_match": False,
|
||||
},
|
||||
proxy_url="http://127.0.0.1:7890",
|
||||
)
|
||||
|
||||
assert normalized["provider_priority"] == ["imap_old", "graph_api"]
|
||||
assert normalized["health_failure_threshold"] == 4
|
||||
assert normalized["health_disable_duration"] == 90
|
||||
assert normalized["require_recipient_match"] is False
|
||||
assert normalized["proxy_url"] == "http://127.0.0.1:7890"
|
||||
|
||||
|
||||
def test_email_parser_has_openai_sender():
|
||||
parser = EmailParser()
|
||||
mails = [
|
||||
EmailMessage(id="x1", subject="hello", sender="notice@example.com"),
|
||||
EmailMessage(id="x2", subject="otp", sender="no-reply@tm.openai.com"),
|
||||
]
|
||||
assert parser.has_openai_sender(mails) is True
|
||||
|
||||
|
||||
def test_outlook_service_returns_early_when_batch_has_no_openai_sender(monkeypatch):
|
||||
service = OutlookService(
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "pwd",
|
||||
},
|
||||
name="outlook-test",
|
||||
)
|
||||
|
||||
non_openai_emails = [
|
||||
EmailMessage(
|
||||
id="m1",
|
||||
subject="newsletter",
|
||||
sender="newsletter@example.com",
|
||||
recipients=["user@example.com"],
|
||||
body="no code",
|
||||
received_timestamp=100,
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_try_providers_for_emails",
|
||||
lambda *args, **kwargs: non_openai_emails,
|
||||
)
|
||||
|
||||
try:
|
||||
service.get_verification_code(
|
||||
email="user@example.com",
|
||||
timeout=30,
|
||||
otp_sent_at=0,
|
||||
)
|
||||
except OTPNoOpenAISenderEmailServiceError:
|
||||
return
|
||||
|
||||
raise AssertionError("expected OTPNoOpenAISenderEmailServiceError")
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import src.core.register as register_module
|
||||
from src.core.register import (
|
||||
ERROR_OTP_TIMEOUT_SECONDARY,
|
||||
PhaseContext,
|
||||
PhaseResult,
|
||||
RegistrationEngine,
|
||||
)
|
||||
from src.services import EmailServiceType
|
||||
@@ -13,6 +14,10 @@ class DummySettings:
|
||||
openai_token_url = "https://token.example.test"
|
||||
openai_redirect_uri = "https://callback.example.test"
|
||||
openai_scope = "openid profile email"
|
||||
email_code_timeout = 120
|
||||
email_code_poll_interval = 3
|
||||
email_code_resend_max_retries = 2
|
||||
email_code_non_openai_sender_resend_max_retries = 1
|
||||
|
||||
|
||||
class FakeEmailService:
|
||||
@@ -26,6 +31,12 @@ class FakeEmailService:
|
||||
return self.code
|
||||
|
||||
|
||||
class FastResendEmailService(FakeEmailService):
|
||||
def get_verification_code(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
raise register_module.OTPNoOpenAISenderEmailServiceError()
|
||||
|
||||
|
||||
class FakeCookies:
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
@@ -56,11 +67,67 @@ class FakeResponse:
|
||||
return self._json_payload
|
||||
|
||||
|
||||
def _build_engine(monkeypatch, email_service):
|
||||
monkeypatch.setattr(register_module, "get_settings", lambda: DummySettings())
|
||||
def _build_engine(monkeypatch, email_service, **setting_overrides):
|
||||
def _settings():
|
||||
settings = DummySettings()
|
||||
for key, value in setting_overrides.items():
|
||||
setattr(settings, key, value)
|
||||
return settings
|
||||
|
||||
monkeypatch.setattr(register_module, "get_settings", _settings)
|
||||
return RegistrationEngine(email_service=email_service)
|
||||
|
||||
|
||||
def _build_failed_phase(error_code: str, error_message: str) -> PhaseResult:
|
||||
return PhaseResult(
|
||||
phase=register_module.PHASE_OTP_SECONDARY,
|
||||
success=False,
|
||||
error_code=error_code,
|
||||
error_message=error_message,
|
||||
retryable=True,
|
||||
next_action="resend_otp",
|
||||
)
|
||||
|
||||
|
||||
def _prepare_engine_for_run(monkeypatch, phase_results, **setting_overrides):
|
||||
engine = _build_engine(
|
||||
monkeypatch,
|
||||
FakeEmailService(code=None),
|
||||
**setting_overrides,
|
||||
)
|
||||
monkeypatch.setattr(register_module.time, "time", lambda: 100.0)
|
||||
|
||||
send_calls = []
|
||||
phase_iter = iter(phase_results)
|
||||
|
||||
def fake_phase_email_prepare():
|
||||
engine.email = "tester@example.com"
|
||||
engine.email_info = {"service_id": "svc-1"}
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(engine, "_phase_email_prepare", fake_phase_email_prepare)
|
||||
monkeypatch.setattr(engine, "_check_ip_location", lambda: (True, "US"))
|
||||
monkeypatch.setattr(engine, "_init_session", lambda: True)
|
||||
monkeypatch.setattr(engine, "_start_oauth", lambda: True)
|
||||
monkeypatch.setattr(engine, "_get_device_id", lambda: "did-1")
|
||||
monkeypatch.setattr(engine, "_check_sentinel", lambda _did: None)
|
||||
monkeypatch.setattr(
|
||||
engine,
|
||||
"_submit_signup_form",
|
||||
lambda _did, _sen_token: type("SignupResult", (), {"success": True, "error_message": ""})(),
|
||||
)
|
||||
monkeypatch.setattr(engine, "_register_password", lambda: (True, "pass-123"))
|
||||
|
||||
def fake_send_verification_code():
|
||||
send_calls.append("send")
|
||||
engine._otp_sent_at = 100.0
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(engine, "_send_verification_code", fake_send_verification_code)
|
||||
monkeypatch.setattr(engine, "_phase_otp_secondary", lambda *_args, **_kwargs: (None, next(phase_iter)))
|
||||
return engine, send_calls
|
||||
|
||||
|
||||
def test_phase_otp_secondary_uses_remaining_budget_from_start_timestamp(monkeypatch):
|
||||
email_service = FakeEmailService(code="654321")
|
||||
engine = _build_engine(monkeypatch, email_service)
|
||||
@@ -101,6 +168,82 @@ def test_phase_otp_secondary_returns_dedicated_timeout_error_code(monkeypatch):
|
||||
assert engine.phase_history[0].error_code == ERROR_OTP_TIMEOUT_SECONDARY
|
||||
|
||||
|
||||
def test_phase_otp_secondary_maps_no_openai_sender_to_resend_action(monkeypatch):
|
||||
email_service = FastResendEmailService(code=None)
|
||||
engine = _build_engine(monkeypatch, email_service)
|
||||
engine.email = "tester@example.com"
|
||||
engine.email_info = {"service_id": "svc-1"}
|
||||
|
||||
monkeypatch.setattr(register_module.time, "time", lambda: 120.0)
|
||||
|
||||
code, phase_result = engine._phase_otp_secondary(
|
||||
PhaseContext(otp_sent_at=80.0),
|
||||
started_at=100.0,
|
||||
)
|
||||
|
||||
assert code is None
|
||||
assert phase_result.success is False
|
||||
assert phase_result.error_code == "OTP_NO_OPENAI_SENDER"
|
||||
assert phase_result.retryable is True
|
||||
assert phase_result.next_action == "resend_otp"
|
||||
|
||||
|
||||
def test_run_uses_dedicated_budget_for_non_openai_sender_resends(monkeypatch):
|
||||
engine, send_calls = _prepare_engine_for_run(
|
||||
monkeypatch,
|
||||
[
|
||||
_build_failed_phase("OTP_NO_OPENAI_SENDER", "detected non-openai sender"),
|
||||
_build_failed_phase(ERROR_OTP_TIMEOUT_SECONDARY, "timeout after dedicated resend"),
|
||||
],
|
||||
email_code_resend_max_retries=0,
|
||||
email_code_non_openai_sender_resend_max_retries=1,
|
||||
)
|
||||
|
||||
result = engine.run()
|
||||
|
||||
assert result.success is False
|
||||
assert result.error_code == ERROR_OTP_TIMEOUT_SECONDARY
|
||||
assert len(send_calls) == 2
|
||||
|
||||
|
||||
def test_run_stops_when_non_openai_sender_budget_is_exhausted(monkeypatch):
|
||||
engine, send_calls = _prepare_engine_for_run(
|
||||
monkeypatch,
|
||||
[
|
||||
_build_failed_phase("OTP_NO_OPENAI_SENDER", "detected non-openai sender"),
|
||||
_build_failed_phase("OTP_NO_OPENAI_SENDER", "detected non-openai sender again"),
|
||||
],
|
||||
email_code_resend_max_retries=2,
|
||||
email_code_non_openai_sender_resend_max_retries=1,
|
||||
)
|
||||
|
||||
result = engine.run()
|
||||
|
||||
assert result.success is False
|
||||
assert result.error_code == "OTP_NO_OPENAI_SENDER"
|
||||
assert len(send_calls) == 2
|
||||
|
||||
|
||||
def test_run_keeps_timeout_budget_after_non_openai_sender_resend(monkeypatch):
|
||||
engine, send_calls = _prepare_engine_for_run(
|
||||
monkeypatch,
|
||||
[
|
||||
_build_failed_phase(ERROR_OTP_TIMEOUT_SECONDARY, "timeout #1"),
|
||||
_build_failed_phase("OTP_NO_OPENAI_SENDER", "detected non-openai sender"),
|
||||
_build_failed_phase(ERROR_OTP_TIMEOUT_SECONDARY, "timeout #2"),
|
||||
_build_failed_phase(ERROR_OTP_TIMEOUT_SECONDARY, "timeout #3"),
|
||||
],
|
||||
email_code_resend_max_retries=2,
|
||||
email_code_non_openai_sender_resend_max_retries=1,
|
||||
)
|
||||
|
||||
result = engine.run()
|
||||
|
||||
assert result.success is False
|
||||
assert result.error_code == ERROR_OTP_TIMEOUT_SECONDARY
|
||||
assert len(send_calls) == 4
|
||||
|
||||
|
||||
def test_advance_login_authorization_sets_otp_anchor_before_password_submit(monkeypatch):
|
||||
email_service = FakeEmailService(code=None)
|
||||
engine = _build_engine(monkeypatch, email_service)
|
||||
|
||||
Reference in New Issue
Block a user