mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-11 10:00:11 +08:00
Merge pull request #106 from ZHOUKAILIAN/fix/issue-80-master
fix: 修复导入邮箱和现有账号场景下重复命中旧验证码的问题
This commit is contained in:
156
tests/test_account_inbox_code_state.py
Normal file
156
tests/test_account_inbox_code_state.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from src.config.constants import EmailServiceType
|
||||
from src.core.register import RegistrationEngine, RegistrationResult
|
||||
from src.database.models import Account, Base
|
||||
from src.database.session import DatabaseSessionManager
|
||||
from src.services.base import BaseEmailService
|
||||
from src.web.routes import accounts as accounts_routes
|
||||
|
||||
|
||||
class DummySettings:
|
||||
openai_client_id = "client-1"
|
||||
openai_auth_url = "https://auth.openai.test/authorize"
|
||||
openai_token_url = "https://auth.openai.test/token"
|
||||
openai_redirect_uri = "https://localhost/callback"
|
||||
openai_scope = "openid profile email offline_access"
|
||||
tempmail_base_url = "https://api.tempmail.test"
|
||||
tempmail_timeout = 30
|
||||
tempmail_max_retries = 3
|
||||
|
||||
|
||||
class FakeStatefulTempmailService(BaseEmailService):
|
||||
def __init__(self, config=None, name=None):
|
||||
super().__init__(EmailServiceType.TEMPMAIL, name)
|
||||
self.messages = [
|
||||
("id:msg-1", "111111"),
|
||||
("id:msg-2", "222222"),
|
||||
]
|
||||
|
||||
def create_email(self, config=None):
|
||||
return {"email": "tester@example.com", "service_id": "token-1"}
|
||||
|
||||
def get_verification_code(
|
||||
self,
|
||||
email: str,
|
||||
email_id: str = None,
|
||||
timeout: int = 120,
|
||||
pattern: str = r"(?<!\d)(\d{6})(?!\d)",
|
||||
otp_sent_at=None,
|
||||
):
|
||||
for marker, code in self.messages:
|
||||
if self._accept_verification_code(email, code, marker):
|
||||
return code
|
||||
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 _build_test_db(name: str) -> DatabaseSessionManager:
|
||||
runtime_dir = Path("tests_runtime")
|
||||
runtime_dir.mkdir(exist_ok=True)
|
||||
db_path = runtime_dir / name
|
||||
if db_path.exists():
|
||||
db_path.unlink()
|
||||
|
||||
manager = DatabaseSessionManager(f"sqlite:///{db_path}")
|
||||
Base.metadata.create_all(bind=manager.engine)
|
||||
return manager
|
||||
|
||||
|
||||
def test_account_inbox_code_persists_verification_state_across_requests(monkeypatch):
|
||||
manager = _build_test_db("account_inbox_code_state.db")
|
||||
|
||||
with manager.session_scope() as session:
|
||||
account = Account(
|
||||
email="tester@example.com",
|
||||
email_service="tempmail",
|
||||
email_service_id="token-1",
|
||||
status="active",
|
||||
extra_data={},
|
||||
)
|
||||
session.add(account)
|
||||
session.commit()
|
||||
session.refresh(account)
|
||||
account_id = account.id
|
||||
|
||||
@contextmanager
|
||||
def fake_get_db():
|
||||
session = manager.SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
monkeypatch.setattr(accounts_routes, "get_db", fake_get_db)
|
||||
monkeypatch.setattr(accounts_routes, "get_settings", lambda: DummySettings())
|
||||
monkeypatch.setattr(
|
||||
"src.services.base.EmailServiceFactory.create",
|
||||
lambda service_type, config, name=None: FakeStatefulTempmailService(config, name),
|
||||
)
|
||||
|
||||
first = asyncio.run(accounts_routes.get_account_inbox_code(account_id))
|
||||
second = asyncio.run(accounts_routes.get_account_inbox_code(account_id))
|
||||
|
||||
assert first["success"] is True
|
||||
assert first["code"] == "111111"
|
||||
assert second["success"] is True
|
||||
assert second["code"] == "222222"
|
||||
|
||||
with manager.session_scope() as session:
|
||||
saved = session.query(Account).filter(Account.id == account_id).first()
|
||||
verification_state = (saved.extra_data or {}).get("verification_state") or {}
|
||||
assert verification_state["used_codes"] == ["111111", "222222"]
|
||||
assert verification_state["seen_messages"] == ["id:msg-1", "id:msg-2"]
|
||||
|
||||
|
||||
def test_save_to_database_persists_verification_state(monkeypatch):
|
||||
manager = _build_test_db("registration_verification_state.db")
|
||||
|
||||
@contextmanager
|
||||
def fake_get_db():
|
||||
session = manager.SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
monkeypatch.setattr("src.core.register.get_db", fake_get_db)
|
||||
monkeypatch.setattr("src.core.register.get_settings", lambda: DummySettings())
|
||||
|
||||
email_service = FakeStatefulTempmailService()
|
||||
email_service._accept_verification_code("tester@example.com", "111111", "id:msg-1")
|
||||
|
||||
engine = RegistrationEngine(email_service=email_service, proxy_url="http://proxy.test")
|
||||
engine.email_info = {"service_id": "token-1"}
|
||||
|
||||
result = RegistrationResult(
|
||||
success=True,
|
||||
email="tester@example.com",
|
||||
password="secret",
|
||||
account_id="acct-1",
|
||||
workspace_id="ws-1",
|
||||
access_token="access-token",
|
||||
refresh_token="refresh-token",
|
||||
id_token="id-token",
|
||||
session_token="session-token",
|
||||
metadata={"registered_at": "2026-03-26T00:00:00"},
|
||||
source="register",
|
||||
)
|
||||
|
||||
assert engine.save_to_database(result) is True
|
||||
|
||||
with manager.session_scope() as session:
|
||||
saved = session.query(Account).filter(Account.email == "tester@example.com").first()
|
||||
verification_state = (saved.extra_data or {}).get("verification_state") or {}
|
||||
assert verification_state["used_codes"] == ["111111"]
|
||||
assert verification_state["seen_messages"] == ["id:msg-1"]
|
||||
523
tests/test_mail_code_reuse_guard.py
Normal file
523
tests/test_mail_code_reuse_guard.py
Normal file
@@ -0,0 +1,523 @@
|
||||
from src.services.duck_mail import DuckMailService
|
||||
from src.services.freemail import FreemailService
|
||||
from src.services.moe_mail import MeoMailEmailService
|
||||
from src.services.temp_mail import TempMailService
|
||||
from src.services.tempmail import TempmailService
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, status_code=200, payload=None, text=""):
|
||||
self.status_code = status_code
|
||||
self._payload = payload
|
||||
self.text = text
|
||||
self.headers = {}
|
||||
|
||||
def json(self):
|
||||
if self._payload is None:
|
||||
raise ValueError("no json payload")
|
||||
return self._payload
|
||||
|
||||
|
||||
class FakeRequestHTTPClient:
|
||||
def __init__(self, responses):
|
||||
self.responses = list(responses)
|
||||
self.calls = []
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
self.calls.append({
|
||||
"method": method,
|
||||
"url": url,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
if not self.responses:
|
||||
raise AssertionError(f"未准备响应: {method} {url}")
|
||||
return self.responses.pop(0)
|
||||
|
||||
|
||||
class FakeGetHTTPClient:
|
||||
def __init__(self, responses):
|
||||
self.responses = list(responses)
|
||||
self.calls = []
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
self.calls.append({
|
||||
"method": "GET",
|
||||
"url": url,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
if not self.responses:
|
||||
raise AssertionError(f"未准备响应: GET {url}")
|
||||
return self.responses.pop(0)
|
||||
|
||||
|
||||
def test_tempmail_service_skips_code_returned_by_previous_fetch():
|
||||
service = TempmailService({"base_url": "https://api.tempmail.test"})
|
||||
service.http_client = FakeGetHTTPClient([
|
||||
FakeResponse(
|
||||
payload={
|
||||
"emails": [
|
||||
{
|
||||
"date": 1000,
|
||||
"from": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"emails": [
|
||||
{
|
||||
"date": 1000,
|
||||
"from": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
},
|
||||
{
|
||||
"date": 1003,
|
||||
"from": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 654321",
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
first_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
email_id="token-1",
|
||||
timeout=1,
|
||||
otp_sent_at=1000,
|
||||
)
|
||||
second_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
email_id="token-1",
|
||||
timeout=1,
|
||||
otp_sent_at=1002,
|
||||
)
|
||||
|
||||
assert first_code == "111111"
|
||||
assert second_code == "654321"
|
||||
|
||||
|
||||
def test_temp_mail_service_skips_code_returned_by_previous_fetch():
|
||||
service = TempMailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"admin_password": "admin-secret",
|
||||
"domain": "example.com",
|
||||
})
|
||||
service.http_client = FakeRequestHTTPClient([
|
||||
FakeResponse(
|
||||
payload={
|
||||
"results": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
"createdAt": "2026-03-19T10:00:00Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"results": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
"createdAt": "2026-03-19T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 654321",
|
||||
"createdAt": "2026-03-19T10:00:03Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
first_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378400,
|
||||
)
|
||||
second_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378402,
|
||||
)
|
||||
|
||||
assert first_code == "111111"
|
||||
assert second_code == "654321"
|
||||
|
||||
|
||||
def test_temp_mail_service_accepts_same_code_from_newer_message():
|
||||
service = TempMailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"admin_password": "admin-secret",
|
||||
"domain": "example.com",
|
||||
})
|
||||
service.http_client = FakeRequestHTTPClient([
|
||||
FakeResponse(
|
||||
payload={
|
||||
"results": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
"createdAt": "2026-03-19T10:00:00Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"results": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
"createdAt": "2026-03-19T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
"createdAt": "2026-03-19T10:00:03Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
first_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378400,
|
||||
)
|
||||
second_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378402,
|
||||
)
|
||||
|
||||
assert first_code == "111111"
|
||||
assert second_code == "111111"
|
||||
|
||||
|
||||
def test_freemail_service_skips_code_returned_by_previous_fetch():
|
||||
service = FreemailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"admin_token": "jwt-token",
|
||||
})
|
||||
service.http_client = FakeRequestHTTPClient([
|
||||
FakeResponse(
|
||||
payload=[
|
||||
{
|
||||
"id": "msg-1",
|
||||
"sender": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"preview": "Your OpenAI verification code is 111111",
|
||||
"verification_code": "111111",
|
||||
"created_at": "2026-03-19T10:00:00Z",
|
||||
}
|
||||
]
|
||||
),
|
||||
FakeResponse(
|
||||
payload=[
|
||||
{
|
||||
"id": "msg-1",
|
||||
"sender": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"preview": "Your OpenAI verification code is 111111",
|
||||
"verification_code": "111111",
|
||||
"created_at": "2026-03-19T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"sender": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"preview": "Your OpenAI verification code is 654321",
|
||||
"verification_code": "654321",
|
||||
"created_at": "2026-03-19T10:00:03Z",
|
||||
},
|
||||
]
|
||||
),
|
||||
])
|
||||
|
||||
first_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378400,
|
||||
)
|
||||
second_code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378402,
|
||||
)
|
||||
|
||||
assert first_code == "111111"
|
||||
assert second_code == "654321"
|
||||
|
||||
|
||||
def test_duck_mail_service_skips_previously_used_code_even_with_small_timestamp_gap():
|
||||
service = DuckMailService({
|
||||
"base_url": "https://api.duckmail.test",
|
||||
"default_domain": "duckmail.sbs",
|
||||
})
|
||||
service.http_client = FakeRequestHTTPClient([
|
||||
FakeResponse(
|
||||
payload={
|
||||
"hydra:member": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"from": {
|
||||
"name": "OpenAI",
|
||||
"address": "noreply@openai.com",
|
||||
},
|
||||
"subject": "Your verification code",
|
||||
"createdAt": "2026-03-19T10:00:01Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"id": "msg-1",
|
||||
"text": "Your OpenAI verification code is 111111",
|
||||
"html": [],
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"hydra:member": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"from": {
|
||||
"name": "OpenAI",
|
||||
"address": "noreply@openai.com",
|
||||
},
|
||||
"subject": "Your verification code",
|
||||
"createdAt": "2026-03-19T10:00:01Z",
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"from": {
|
||||
"name": "OpenAI",
|
||||
"address": "noreply@openai.com",
|
||||
},
|
||||
"subject": "Your verification code",
|
||||
"createdAt": "2026-03-19T10:00:03Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"id": "msg-2",
|
||||
"text": "Your OpenAI verification code is 654321",
|
||||
"html": [],
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"id": "msg-1",
|
||||
"text": "Your OpenAI verification code is 111111",
|
||||
"html": [],
|
||||
}
|
||||
),
|
||||
])
|
||||
service._accounts_by_email["tester@duckmail.sbs"] = {
|
||||
"email": "tester@duckmail.sbs",
|
||||
"service_id": "account-1",
|
||||
"account_id": "account-1",
|
||||
"token": "token-123",
|
||||
}
|
||||
|
||||
first_code = service.get_verification_code(
|
||||
email="tester@duckmail.sbs",
|
||||
email_id="account-1",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378401,
|
||||
)
|
||||
second_code = service.get_verification_code(
|
||||
email="tester@duckmail.sbs",
|
||||
email_id="account-1",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378402,
|
||||
)
|
||||
|
||||
assert first_code == "111111"
|
||||
assert second_code == "654321"
|
||||
|
||||
|
||||
def test_moe_mail_service_filters_old_messages_with_millisecond_timestamps():
|
||||
service = MeoMailEmailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"api_key": "api-key",
|
||||
})
|
||||
|
||||
def fake_make_request(method, endpoint, **kwargs):
|
||||
if endpoint == "/api/emails/email-1":
|
||||
return {
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-old",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378400000,
|
||||
},
|
||||
{
|
||||
"id": "msg-new",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378403000,
|
||||
},
|
||||
]
|
||||
}
|
||||
if endpoint == "/api/emails/email-1/msg-old":
|
||||
return {
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 111111",
|
||||
}
|
||||
}
|
||||
if endpoint == "/api/emails/email-1/msg-new":
|
||||
return {
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 654321",
|
||||
}
|
||||
}
|
||||
raise AssertionError(f"未准备响应: {method} {endpoint}")
|
||||
|
||||
service._make_request = fake_make_request
|
||||
|
||||
code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
email_id="email-1",
|
||||
timeout=1,
|
||||
otp_sent_at=1742378402,
|
||||
)
|
||||
|
||||
assert code == "654321"
|
||||
|
||||
|
||||
def test_moe_mail_service_cross_request_state_prefers_latest_of_three_messages():
|
||||
first_service = MeoMailEmailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"api_key": "api-key",
|
||||
})
|
||||
|
||||
first_responses = [
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378400000,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 111111",
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
def fake_make_request_first(method, endpoint, **kwargs):
|
||||
if not first_responses:
|
||||
raise AssertionError(f"未准备响应: {method} {endpoint}")
|
||||
return first_responses.pop(0)
|
||||
|
||||
first_service._make_request = fake_make_request_first
|
||||
|
||||
first_code = first_service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
email_id="email-1",
|
||||
timeout=1,
|
||||
)
|
||||
state = first_service.export_verification_state("tester@example.com")
|
||||
|
||||
second_service = MeoMailEmailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"api_key": "api-key",
|
||||
})
|
||||
second_service.load_verification_state("tester@example.com", **state)
|
||||
|
||||
second_calls = []
|
||||
second_responses = {
|
||||
"/api/emails/email-1": {
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378400000,
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378403000,
|
||||
},
|
||||
{
|
||||
"id": "msg-3",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378406000,
|
||||
},
|
||||
]
|
||||
},
|
||||
"/api/emails/email-1/msg-3": {
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 333333",
|
||||
}
|
||||
},
|
||||
"/api/emails/email-1/msg-2": {
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 222222",
|
||||
}
|
||||
},
|
||||
"/api/emails/email-1/msg-1": {
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 111111",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def fake_make_request_second(method, endpoint, **kwargs):
|
||||
second_calls.append(endpoint)
|
||||
if endpoint not in second_responses:
|
||||
raise AssertionError(f"未准备响应: {method} {endpoint}")
|
||||
return second_responses[endpoint]
|
||||
|
||||
second_service._make_request = fake_make_request_second
|
||||
|
||||
second_code = second_service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
email_id="email-1",
|
||||
timeout=1,
|
||||
)
|
||||
|
||||
assert first_code == "111111"
|
||||
assert state == {
|
||||
"used_codes": ["111111"],
|
||||
"seen_messages": ["id:msg-1"],
|
||||
}
|
||||
assert second_code == "333333"
|
||||
assert second_calls == [
|
||||
"/api/emails/email-1",
|
||||
"/api/emails/email-1/msg-3",
|
||||
]
|
||||
258
tests/test_mail_latest_code_preference.py
Normal file
258
tests/test_mail_latest_code_preference.py
Normal file
@@ -0,0 +1,258 @@
|
||||
from src.services.duck_mail import DuckMailService
|
||||
from src.services.freemail import FreemailService
|
||||
from src.services.moe_mail import MeoMailEmailService
|
||||
from src.services.temp_mail import TempMailService
|
||||
from src.services.tempmail import TempmailService
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, status_code=200, payload=None, text=""):
|
||||
self.status_code = status_code
|
||||
self._payload = payload
|
||||
self.text = text
|
||||
self.headers = {}
|
||||
|
||||
def json(self):
|
||||
if self._payload is None:
|
||||
raise ValueError("no json payload")
|
||||
return self._payload
|
||||
|
||||
|
||||
class FakeRequestHTTPClient:
|
||||
def __init__(self, responses):
|
||||
self.responses = list(responses)
|
||||
self.calls = []
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
self.calls.append({
|
||||
"method": method,
|
||||
"url": url,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
if not self.responses:
|
||||
raise AssertionError(f"未准备响应: {method} {url}")
|
||||
return self.responses.pop(0)
|
||||
|
||||
|
||||
class FakeGetHTTPClient:
|
||||
def __init__(self, responses):
|
||||
self.responses = list(responses)
|
||||
self.calls = []
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
self.calls.append({
|
||||
"method": "GET",
|
||||
"url": url,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
if not self.responses:
|
||||
raise AssertionError(f"未准备响应: GET {url}")
|
||||
return self.responses.pop(0)
|
||||
|
||||
|
||||
def test_tempmail_service_prefers_latest_matching_message_without_otp_timestamp():
|
||||
service = TempmailService({"base_url": "https://api.tempmail.test"})
|
||||
service.http_client = FakeGetHTTPClient([
|
||||
FakeResponse(
|
||||
payload={
|
||||
"emails": [
|
||||
{
|
||||
"date": 1000,
|
||||
"from": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
},
|
||||
{
|
||||
"date": 1003,
|
||||
"from": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 654321",
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
email_id="token-1",
|
||||
timeout=1,
|
||||
)
|
||||
|
||||
assert code == "654321"
|
||||
|
||||
|
||||
def test_temp_mail_service_prefers_latest_matching_message_without_otp_timestamp():
|
||||
service = TempMailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"admin_password": "admin-secret",
|
||||
"domain": "example.com",
|
||||
})
|
||||
service.http_client = FakeRequestHTTPClient([
|
||||
FakeResponse(
|
||||
payload={
|
||||
"results": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 111111",
|
||||
"createdAt": "2026-03-19T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"source": "OpenAI <noreply@openai.com>",
|
||||
"subject": "Your verification code",
|
||||
"body": "Your OpenAI verification code is 654321",
|
||||
"createdAt": "2026-03-19T10:00:03Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
)
|
||||
|
||||
assert code == "654321"
|
||||
|
||||
|
||||
def test_moe_mail_service_prefers_latest_matching_message_without_otp_timestamp():
|
||||
service = MeoMailEmailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"api_key": "api-key",
|
||||
})
|
||||
|
||||
def fake_make_request(method, endpoint, **kwargs):
|
||||
if endpoint == "/api/emails/email-1":
|
||||
return {
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378400000,
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"from_address": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"received_at": 1742378403000,
|
||||
},
|
||||
]
|
||||
}
|
||||
if endpoint == "/api/emails/email-1/msg-1":
|
||||
return {
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 111111",
|
||||
}
|
||||
}
|
||||
if endpoint == "/api/emails/email-1/msg-2":
|
||||
return {
|
||||
"message": {
|
||||
"content": "Your OpenAI verification code is 654321",
|
||||
}
|
||||
}
|
||||
raise AssertionError(f"未准备响应: {method} {endpoint}")
|
||||
|
||||
service._make_request = fake_make_request
|
||||
|
||||
code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
email_id="email-1",
|
||||
timeout=1,
|
||||
)
|
||||
|
||||
assert code == "654321"
|
||||
|
||||
|
||||
def test_freemail_service_prefers_latest_matching_message_without_otp_timestamp():
|
||||
service = FreemailService({
|
||||
"base_url": "https://mail.example.com",
|
||||
"admin_token": "jwt-token",
|
||||
})
|
||||
service.http_client = FakeRequestHTTPClient([
|
||||
FakeResponse(
|
||||
payload=[
|
||||
{
|
||||
"id": "msg-1",
|
||||
"sender": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"preview": "Your OpenAI verification code is 111111",
|
||||
"verification_code": "111111",
|
||||
"created_at": "2026-03-19T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"sender": "noreply@openai.com",
|
||||
"subject": "Your verification code",
|
||||
"preview": "Your OpenAI verification code is 654321",
|
||||
"verification_code": "654321",
|
||||
"created_at": "2026-03-19T10:00:03Z",
|
||||
},
|
||||
]
|
||||
),
|
||||
])
|
||||
|
||||
code = service.get_verification_code(
|
||||
email="tester@example.com",
|
||||
timeout=1,
|
||||
)
|
||||
|
||||
assert code == "654321"
|
||||
|
||||
|
||||
def test_duck_mail_service_prefers_latest_matching_message_without_otp_timestamp():
|
||||
service = DuckMailService({
|
||||
"base_url": "https://api.duckmail.test",
|
||||
"default_domain": "duckmail.sbs",
|
||||
})
|
||||
service.http_client = FakeRequestHTTPClient([
|
||||
FakeResponse(
|
||||
payload={
|
||||
"hydra:member": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"from": {
|
||||
"name": "OpenAI",
|
||||
"address": "noreply@openai.com",
|
||||
},
|
||||
"subject": "Your verification code",
|
||||
"createdAt": "2026-03-19T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "msg-2",
|
||||
"from": {
|
||||
"name": "OpenAI",
|
||||
"address": "noreply@openai.com",
|
||||
},
|
||||
"subject": "Your verification code",
|
||||
"createdAt": "2026-03-19T10:00:03Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
),
|
||||
FakeResponse(
|
||||
payload={
|
||||
"id": "msg-2",
|
||||
"text": "Your OpenAI verification code is 654321",
|
||||
"html": [],
|
||||
}
|
||||
),
|
||||
])
|
||||
service._accounts_by_email["tester@duckmail.sbs"] = {
|
||||
"email": "tester@duckmail.sbs",
|
||||
"service_id": "account-1",
|
||||
"account_id": "account-1",
|
||||
"token": "token-123",
|
||||
}
|
||||
|
||||
code = service.get_verification_code(
|
||||
email="tester@duckmail.sbs",
|
||||
email_id="account-1",
|
||||
timeout=1,
|
||||
)
|
||||
|
||||
assert code == "654321"
|
||||
92
tests/test_register_otp_integration.py
Normal file
92
tests/test_register_otp_integration.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.core.register import RegistrationEngine
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, status_code=200, payload=None, text=""):
|
||||
self.status_code = status_code
|
||||
self._payload = payload
|
||||
self.text = text
|
||||
self.headers = {}
|
||||
|
||||
def json(self):
|
||||
if self._payload is None:
|
||||
raise ValueError("no json payload")
|
||||
return self._payload
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self):
|
||||
self.get_calls = []
|
||||
self.post_calls = []
|
||||
self.cookies = SimpleNamespace(get=lambda name: None)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
self.get_calls.append({
|
||||
"url": url,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
return FakeResponse(status_code=200)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
self.post_calls.append({
|
||||
"url": url,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
if url.endswith("/email-otp/validate"):
|
||||
code = json.loads(kwargs["data"])["code"]
|
||||
return FakeResponse(status_code=200 if code == "654321" else 401)
|
||||
return FakeResponse(status_code=200)
|
||||
|
||||
|
||||
class FakeEmailService:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def get_verification_code(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
if len(self.calls) == 1:
|
||||
return None
|
||||
return "654321"
|
||||
|
||||
|
||||
def test_registration_engine_resend_flow_propagates_new_otp_timestamp_and_validates_code(monkeypatch):
|
||||
engine = RegistrationEngine.__new__(RegistrationEngine)
|
||||
engine.logs = []
|
||||
engine._log = lambda message, level="info": None
|
||||
engine.email = "tester@example.com"
|
||||
engine.email_info = {"service_id": "email-1"}
|
||||
engine.email_service = FakeEmailService()
|
||||
engine.session = FakeSession()
|
||||
engine._otp_sent_at = None
|
||||
engine.phase_history = []
|
||||
|
||||
issued_timestamps = iter([1000.0, 1000.0, 1000.0, 1005.0, 1005.0, 1005.0])
|
||||
monkeypatch.setattr("src.core.register.time.time", lambda: next(issued_timestamps))
|
||||
|
||||
assert engine._send_verification_code() is True
|
||||
first_otp_sent_at = engine._otp_sent_at
|
||||
first_code = engine._get_verification_code()
|
||||
|
||||
assert first_otp_sent_at == 1000.0
|
||||
assert first_code is None
|
||||
|
||||
assert engine._send_verification_code() is True
|
||||
second_otp_sent_at = engine._otp_sent_at
|
||||
second_code = engine._get_verification_code()
|
||||
|
||||
assert second_otp_sent_at == 1005.0
|
||||
assert second_code == "654321"
|
||||
assert engine._validate_verification_code(second_code) is True
|
||||
|
||||
assert len(engine.email_service.calls) == 2
|
||||
assert engine.email_service.calls[0]["otp_sent_at"] == 1000.0
|
||||
assert engine.email_service.calls[1]["otp_sent_at"] == 1005.0
|
||||
assert engine.email_service.calls[0]["email_id"] == "email-1"
|
||||
assert engine.email_service.calls[1]["email_id"] == "email-1"
|
||||
|
||||
validate_call = engine.session.post_calls[-1]
|
||||
assert validate_call["url"].endswith("/email-otp/validate")
|
||||
assert json.loads(validate_call["kwargs"]["data"]) == {"code": "654321"}
|
||||
Reference in New Issue
Block a user