看图时发现两个问题: 一、页面上那段说明是 P1a 时代的文案,已经过时且误导:「重登仍由人工在 「CK 检测与重登」页面操作」——P1b 上线后自动重登已存在。两个作业其实是联动的 上下游:巡检把账号标成 invalid,「CK 自动重登」的候选条件正是这个字段,所以 巡检一发现失效、重登作业下一周期就会接手。刻意分成两个作业是为了止损粒度 (巡检只读零风险;重登会真实登录,出问题可单独关掉)。据此改写说明文案,并在 两张卡片上标出「下游:CK 自动重登」/「上游:巡检标记为失效且有 CK 的账号」。 二、jobs 接口新增 pending_relogin:当前有多少条正等着自动重登。页面靠它把 上下游关系显性化 —— 没有这个数字,两个开关看起来像互不相干。计数走重登作业 自己的候选查询,保证「页面显示的待办」和「作业实际会捞的」是同一口径。 顺带修掉 tests/test_douyu_email_code_pick.py 里一条既有缺陷:它用 max(0, now.minute - 30) 构造「30 分钟前的邮件」,分钟数小于 30 时会退化成当前 整点(21:00:12 → "今天 21:00"),只比 now 早十几秒,于是断言随「当前几点」偶发 失败 —— 每小时前 30 分钟必红(已确认在我本次所有改动之前的提交上同样失败, 不是本次引入)。改成真实的时间减法,跨天时用「昨天」。 全量 298 通过。提交时另一路 SQLite→MySQL 的测试改造仍在同一工作区进行, 本次只暂存本功能相关文件。
122 lines
4.7 KiB
Python
122 lines
4.7 KiB
Python
"""回归:取验证码必须取「本次发送触发的那封」新邮件。
|
|
|
|
线上现象:同一个邮箱每次登录都取到同一个旧验证码(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"<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 快照时,比发送时间早的邮件必须被跳过。
|
|
|
|
注意这里必须用**真实的时间减法**构造「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
|