Files
live-hub-py/tests/test_login_attempt_migration.py
T
yml2213 6a25097412 fix(douyu): 替换 CK 是最后一步,只有登录成功才写回记录
上一版把可见性改成「记录里还有 CK」,账号不会再消失,但根因还在:批次启动
时就把复用的成功记录改成 pending(旧 CK 仍在),停止或失败后没人恢复状态,
记录一直显示成一次没完成的登录。

按「替换 CK 是最后一步」把两者拆开:

- login_tasks 新增 attempt_status / attempt_message / attempt_batch_id /
  attempt_at,批次只写 attempt_*,status/cookie/message 只在登录成功时更新;
- _begin_attempt 只登记尝试,_mark_attempt_failed 只记录失败(已有 CK 的记录
  原样不动),_write_login_success 是唯一替换旧 CK 的入口;
- 停止批次、代理不可用、登录异常、进程重启清理全部改为收敛 attempt_*,
  因此停止一个批次不可能再让账号失去 CK;
- 复用的记录改用「持有 CK 的记录」,不再创建第二条记录,失败记录在无 CK 时
  才复用;
- 登录任务列表/统计按 attempt_* 展示「这次登录怎么样了」,批次筛选同时匹配
  attempt_batch_id;CK 操作页的重登状态也从 attempt_* 映射;
- 迁移 20261001_0035 归一存量数据:持有 CK 的记录 status 统一为 success、
  历史状态搬进 attempt_*,无 CK 且卡在 pending/running 的收敛为 failed。

新增测试:批次失败/停止不碰旧 CK、成功后替换 CK、列表展示尝试状态、
迁移存量数据归一。
2026-10-01 18:29:49 +08:00

153 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""迁移 20261001_0035 的存量数据归一验证。
线上升级时要保证:
- 持有 CK 的记录 status 归一到 success,历史批次状态搬进 attempt_*;
- 没有 CK 且卡在 pending/running 的记录收敛为 failed;
- 任何情况下都不丢 CK。
"""
import tempfile
from pathlib import Path
from alembic import command
from alembic.config import Config
from sqlalchemy import create_engine, text
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def _migration_config(url: str) -> Config:
config = Config(str(PROJECT_ROOT / "alembic.ini"))
config.set_main_option(
"script_location", str(PROJECT_ROOT / "web" / "backend" / "migrations")
)
config.set_main_option("sqlalchemy.url", url)
return config
def _insert_task(conn, *, status: str, cookie: str, message: str = "") -> None:
conn.execute(
text(
"""
INSERT INTO login_tasks
(batch_id, account_id, status, cookie, message, created_by,
created_at, finished_at)
VALUES
('batch-1', 1, :status, :cookie, :message, 1,
'2026-09-30 00:00:00', '2026-09-30 01:00:00')
"""
),
{"status": status, "cookie": cookie, "message": message},
)
class TestLoginAttemptMigration:
def setup_method(self):
import web.backend.database as database_module
self.database_module = database_module
self.original_url = database_module.DATABASE_URL
self.tmpdir = tempfile.TemporaryDirectory()
self.url = f"sqlite:///{Path(self.tmpdir.name) / 'upgrade.db'}"
# env.py 用模块级 DATABASE_URL 覆盖配置里的连接串。
database_module.DATABASE_URL = self.url
self.config = _migration_config(self.url)
command.upgrade(self.config, "20260901_0034")
self.engine = create_engine(self.url)
with self.engine.begin() as conn:
conn.execute(
text(
"""
INSERT INTO accounts
(id, username, password, email, email_password,
email_imap_server, email_imap_port, email_imap_ssl,
tag, remark, uid, nickname, created_at, updated_at)
VALUES
(1, '远行者103', 'p', 'e@example.com', 'm',
'', 993, 1, '', '', '', '', '2026-09-30 00:00:00',
'2026-09-30 00:00:00')
"""
)
)
conn.execute(
text(
"""
INSERT INTO users (id, username, password_hash, role, is_active)
VALUES (1, 'admin', 'hash', 'super_admin', 1)
"""
)
)
_insert_task(conn, status="success", cookie="ck-success")
_insert_task(
conn,
status="relogin_failed",
cookie="ck-relogin",
message="重新登录失败: 密码错误(旧 Cookie 已保留)",
)
_insert_task(conn, status="pending", cookie="ck-pending")
_insert_task(conn, status="pending", cookie="")
_insert_task(conn, status="running", cookie="")
_insert_task(conn, status="failed", cookie="ck-failed", message="登录失败")
def teardown_method(self):
self.database_module.DATABASE_URL = self.original_url
self.engine.dispose()
self.tmpdir.cleanup()
def _rows(self) -> list[dict]:
with self.engine.connect() as conn:
rows = conn.execute(
text(
"""
SELECT status, cookie, message, attempt_status,
attempt_message, attempt_batch_id, attempt_at
FROM login_tasks
ORDER BY id
"""
)
).mappings()
return [dict(row) for row in rows]
def test_upgrade_normalizes_existing_rows(self):
command.upgrade(self.config, "head")
rows = self._rows()
(
success,
relogin_failed,
pending_with_ck,
pending_without_ck,
running_without_ck,
failed_with_ck,
) = rows
# 已经成功的记录:status 不变,也没有历史尝试需要搬迁
assert success["status"] == "success"
assert success["cookie"] == "ck-success"
assert success["attempt_status"] in (None, "")
# 重登失败:CK 保留,status 归一为 success,失败信息进 attempt_*
assert relogin_failed["cookie"] == "ck-relogin"
assert relogin_failed["status"] == "success"
assert relogin_failed["attempt_status"] == "failed"
assert "旧 Cookie 已保留" in relogin_failed["attempt_message"]
assert relogin_failed["attempt_batch_id"] == "batch-1"
# 卡在 pending 但持有 CK:同样归一为 success + 中断说明
assert pending_with_ck["cookie"] == "ck-pending"
assert pending_with_ck["status"] == "success"
assert pending_with_ck["attempt_status"] == "failed"
assert "旧 Cookie 已保留" in pending_with_ck["attempt_message"]
# 没有 CK 的在途记录:收敛为失败,不再永久排队
for row in (pending_without_ck, running_without_ck):
assert row["status"] == "failed"
assert row["attempt_status"] == "failed"
assert "服务重启" in row["message"]
# 复用记录上的旧 CK 不能被清掉
assert failed_with_ck["status"] == "success"
assert failed_with_ck["cookie"] == "ck-failed"
assert failed_with_ck["attempt_status"] == "failed"