"""回归:取验证码必须取「本次发送触发的那封」新邮件。 线上现象:同一个邮箱每次登录都取到同一个旧验证码(UID=1,几小时前那封), 提交必然报「邮箱验证码错误」。两个原因: 1. Roundcube 返回的邮件列表本来就是「UID 降序(最新在前)」,代码又 ``reversed()`` 了一次,于是变成从最旧的一封开始读; 2. ``_parse_rc_date`` 用 UTC 作为基准,而 Roundcube 显示的是邮箱本地时间, 旧邮件被凭空加了 8 小时,看起来比发送时间还新,时间过滤形同虚设。 现在改为:发信前记录收件箱里最新的斗鱼邮件 UID,之后只接受 UID 更大的邮件; 日期解析按本地时间,仅在拿不到快照时才用时间兜底。 """ from datetime import UTC, datetime, timedelta 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"
您的验证码是 {code}
" 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 快照时,比发送时间早的邮件必须被跳过。 注意这里必须用**真实的时间减法**构造「30 分钟前」,不能写成 ``max(0, now.minute - 30)``:分钟数小于 30 时它会退化成当前整点 (如 21:00:12 得到 "今天 21:00"),只比 now 早十几秒,断言就随 「当前是几点」偶发失败 —— 每小时前 30 分钟必红。跨天时用「昨天」。 """ now = datetime.now(tz=UTC).astimezone() earlier = now - timedelta(minutes=30) prefix = "今天" if earlier.date() == now.date() else "昨天" verifier = _FakeVerifier( [_msg(1, f"{prefix} {earlier.hour:02d}:{earlier.minute: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