fix(garmin): 退出账号会把邮箱和令牌一起永久删掉,SSO 账号无处补救

用户刚才绑定失败:`Request failed with status code 400`。查下来是
`delete_token()`("退出 Garmin 账号"背后的函数)的注释说"邮箱留在 user 表,
下次只需要密码",但代码只有 `DELETE FROM garmin_tokens`——而 `garmin_email`
唯一的落脚点就是这张表。`users.garmin_email` 是本地密码登录时代的遗留列,
`get_remembered_email` 里写得很清楚:auth-hub 接管之后,没有任何绑定路径再
往那张表写过东西。也就是说现在**所有账号**(auth-hub SSO)退出一次,等于
永久忘记邮箱——注释描述的"安全网"对这些账号从来没生效过。

- `delete_token` 删除前把 `garmin_tokens.garmin_email` 复制一份到
  `users.garmin_email`,注释里承诺的行为终于是真的
- 顺带修了一条原有测试掩盖真相的问题:`test_disconnecting_drops_the_
  current_binding_but_not_the_legacy_value` 预先在 users 表塞了一条 legacy
  邮箱,退出后自然能读到——从来没测过 SSO 账号真正会遇到的情况(legacy 列
  本来就是空的)。新增两条测试覆盖这个和"连续退出两次不会把邮箱也搞丢"
- 生产账号的邮箱已经从退出前的 dump 备份里手工恢复,不用重新绑定就能补上

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ericwyuan
2026-09-13 07:20:52 +08:00
parent 4331a07462
commit db7f182030
2 changed files with 42 additions and 5 deletions

View File

@@ -338,16 +338,41 @@ class TestRememberedEmail:
)
assert garmin_svc.get_remembered_email(user["id"]) == "legacy@example.com"
def test_disconnecting_drops_the_current_binding_but_not_the_legacy_value(
def test_disconnecting_remembers_the_most_recent_email_not_a_stale_legacy_one(
self, db, user
):
"""The current binding's email is copied forward on disconnect (see
`delete_token`), so it wins over whatever older address happened to be
sitting in the legacy column — the account last used `current@…`, and
that is what the next bind attempt should be offered."""
db.execute(
"UPDATE users SET garmin_email = ? WHERE id = ?",
["legacy@example.com", user["id"]],
)
garmin_svc.save_token(user["id"], "tok", "current@example.com")
garmin_svc.delete_token(user["id"])
assert garmin_svc.get_remembered_email(user["id"]) == "legacy@example.com"
assert garmin_svc.get_remembered_email(user["id"]) == "current@example.com"
def test_disconnecting_an_auth_hub_account_still_remembers_the_email(
self, db, user
):
"""The case the test above does not cover, and the one that actually
broke: an auth-hub account has no pre-existing legacy row — the only
copy of the email is the one `delete_token` is about to remove. Without
copying it forward first, 退出 Garmin 账号 silently breaks its own
"只需一次" promise, and the next bind attempt 400s on a blank email
with no visible link back to the disconnect that caused it.
"""
assert garmin_svc.get_remembered_email(user["id"]) == ""
garmin_svc.save_token(user["id"], "tok", "current@example.com")
garmin_svc.delete_token(user["id"])
assert garmin_svc.get_remembered_email(user["id"]) == "current@example.com"
def test_disconnecting_twice_does_not_forget_the_email(self, db, user):
garmin_svc.save_token(user["id"], "tok", "current@example.com")
garmin_svc.delete_token(user["id"])
garmin_svc.delete_token(user["id"]) # no token row left to read from
assert garmin_svc.get_remembered_email(user["id"]) == "current@example.com"
class TestMfaHandling: