Files
codex-register/check_otp_timing.py

156 lines
4.5 KiB
Python

#!/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())