Files
live-hub-py/tests/test_douyu_email_code_pick.py
T
yml2213 4d87cdc16d fix(douyu): 取邮箱验证码取错邮件,导致登录永远报「邮箱验证码错误」
线上现象:同一个邮箱每次登录都取到同一个旧验证码(UID=1、几小时前那封),
提交必然失败;整体重试 20 次又把斗鱼的发信配额打爆。

两个根因:

1. Roundcube 返回的邮件列表本来就是 UID 降序(最新在前),代码又 reversed()
   了一次,于是从收件箱最旧的一封开始读;
2. _parse_rc_date 用 UTC 作为基准,而 Roundcube 显示的是邮箱本地时间,旧邮件
   被凭空加了 8 小时,看起来比发送时间还新,时间过滤形同虚设。

改动:

- 发信前用 latest_douyu_uid() 记录收件箱最新斗鱼邮件 UID,之后只接受 UID
  更大的邮件,精确锁定「本次发送触发的那封」,不再依赖列表顺序和日期解析;
- 明确按 UID 降序读取,去掉多余的 reversed();
- 日期解析改为本地时区 aware datetime,时间兜底过滤才真正生效;
- 斗鱼返回「邮箱验证码发送次数已达上限」时抛 EmailQuotaError,直接终止批次
  不再重试,避免继续烧配额。

线上实测(忧伤的心750 / aakpfc@bdhg.xyz):修复前每次读 UID=1 的旧码
942715;修复后基线 UID=20、按发送时间与快照过滤均正确跳过旧邮件,正文读取
取到最新一封。
2026-10-01 18:42:56 +08:00

114 lines
4.2 KiB
Python

"""回归:取验证码必须取「本次发送触发的那封」新邮件。
线上现象:同一个邮箱每次登录都取到同一个旧验证码(UID=1,几小时前那封),
提交必然报「邮箱验证码错误」。两个原因:
1. Roundcube 返回的邮件列表本来就是「UID 降序(最新在前)」,代码又
``reversed()`` 了一次,于是变成从最旧的一封开始读;
2. ``_parse_rc_date`` 用 UTC 作为基准,而 Roundcube 显示的是邮箱本地时间,
旧邮件被凭空加了 8 小时,看起来比发送时间还新,时间过滤形同虚设。
现在改为:发信前记录收件箱里最新的斗鱼邮件 UID,之后只接受 UID 更大的邮件;
日期解析按本地时间,仅在拿不到快照时才用时间兜底。
"""
from datetime import UTC, datetime
from core.douyu.email_verifier import EmailVerifier
SUBJECT = "验证码 - 斗鱼"
class _FakeVerifier(EmailVerifier):
"""把 Roundcube 的网络访问换成内存数据。"""
def __init__(self, messages: list[dict], codes: dict[int, str]):
super().__init__(username="user@example.com", password="password")
self._messages = messages
self._codes = codes
self.read_uids: list[int] = []
def _rc_fetch_mail_list(self) -> list[dict]:
return list(self._messages)
def _rc_fetch_email_body(self, uid: int) -> str | None:
self.read_uids.append(uid)
code = self._codes.get(uid)
return f"<div>您的验证码是 {code}</div>" if code else None
def _msg(uid: int, date: str) -> dict:
return {"uid": uid, "subject": SUBJECT, "date": date}
def _newest_first(uids: list[int]) -> list[dict]:
"""Roundcube 实际返回的顺序:UID 从大到小。"""
return [_msg(uid, "今天 12:00") for uid in sorted(uids, reverse=True)]
class TestEmailCodePick:
def test_reads_newest_mail_when_list_is_newest_first(self):
verifier = _FakeVerifier(
_newest_first([1, 2, 3]),
{1: "111111", 2: "222222", 3: "333333"},
)
assert verifier._fetch_code_via_roundcube() == "333333"
assert verifier.read_uids == [3]
def test_reads_newest_mail_when_list_is_oldest_first(self):
verifier = _FakeVerifier(
[_msg(uid, "今天 12:00") for uid in (1, 2, 3)],
{1: "111111", 2: "222222", 3: "333333"},
)
assert verifier._fetch_code_via_roundcube() == "333333"
def test_min_uid_waits_for_a_newer_mail(self):
verifier = _FakeVerifier(
_newest_first([1, 2, 3]),
{1: "111111", 2: "222222", 3: "333333"},
)
# 发信前最新是 UID=3,还没收到新邮件时不应返回任何旧验证码
assert verifier._fetch_code_via_roundcube(min_uid=3) is None
verifier._messages = _newest_first([1, 2, 3, 4])
verifier._codes[4] = "444444"
assert verifier._fetch_code_via_roundcube(min_uid=3) == "444444"
assert verifier.read_uids == [4]
def test_send_time_filter_skips_older_mail(self):
"""没有 UID 快照时,比发送时间早的邮件必须被跳过。"""
now = datetime.now(tz=UTC).astimezone()
verifier = _FakeVerifier(
[_msg(1, f"今天 {now.hour:02d}:{max(0, now.minute - 30):02d}")],
{1: "111111"},
)
assert (
verifier._fetch_code_via_roundcube(after_timestamp=now.timestamp()) is None
)
assert verifier.read_uids == []
def test_parse_rc_date_uses_local_time(self):
now = datetime.now(tz=UTC).astimezone()
parsed = EmailVerifier._parse_rc_date("今天 18:37")
assert parsed is not None
assert (parsed.hour, parsed.minute) == (18, 37)
# 同一天,且不是 UTC 解释(否则在 UTC+8 会差 8 小时)
assert parsed.date() == now.replace(hour=18, minute=37).date()
def test_latest_douyu_uid_ignores_other_mail(self):
verifier = _FakeVerifier(
[
{"uid": 9, "subject": "广告邮件", "date": "今天 12:00"},
_msg(4, "今天 11:00"),
_msg(2, "今天 10:00"),
],
{},
)
assert verifier.latest_douyu_uid() == 4