fix: 扩展邮箱验证码去重到现有账号链路

This commit is contained in:
zhoukailian
2026-03-26 14:34:11 +08:00
parent eda7cbc71f
commit 1bc53b6569
8 changed files with 475 additions and 8 deletions

View 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"]

View File

@@ -325,15 +325,15 @@ def test_duck_mail_service_skips_previously_used_code_even_with_small_timestamp_
),
FakeResponse(
payload={
"id": "msg-1",
"text": "Your OpenAI verification code is 111111",
"id": "msg-2",
"text": "Your OpenAI verification code is 654321",
"html": [],
}
),
FakeResponse(
payload={
"id": "msg-2",
"text": "Your OpenAI verification code is 654321",
"id": "msg-1",
"text": "Your OpenAI verification code is 111111",
"html": [],
}
),

View 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"