From 60c8fece2399d15cee77b952c35f40070eaae5b4 Mon Sep 17 00:00:00 2001 From: guke Date: Thu, 30 Jul 2026 16:02:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(sms):=20verify=5Fcode=20try-both=20?= =?UTF-8?q?=E9=81=8D=E5=8E=86=E5=90=AF=E7=94=A8=E7=9A=84=20fallback=20?= =?UTF-8?q?=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- app/integrations/sms/__init__.py | 16 +++++++++-- tests/test_sms_fallback.py | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/app/integrations/sms/__init__.py b/app/integrations/sms/__init__.py index a676c08..90191bb 100644 --- a/app/integrations/sms/__init__.py +++ b/app/integrations/sms/__init__.py @@ -56,5 +56,17 @@ def send_code(phone: str) -> SendResult: def verify_code(phone: str, code: str) -> bool: - """校验:委托当前主 provider(try-both 在 Task 5 加)。""" - return _primary().verify_code(phone, code) + """校验:try-both,遍历「启用的 fallback 链」(主→备),任一命中即 True。 + + 码只存在实际发码那家(fallback 前主已 pop 掉自己的码),另一家 rec is None 即 False、 + 不误判、不累加其防爆破计数。关闭 fallback 时链中只有主,备完全不参与。 + """ + chain = [_primary()] + fb = _fallback() + if fb is not None: + chain.append(fb) + for prov in chain: + if prov.verify_code(phone, code): # Mode B:纯本地内存比对,不联网 + logger.info("[SMS] verify hit provider=%s", _NAME[prov]) + return True + return False diff --git a/tests/test_sms_fallback.py b/tests/test_sms_fallback.py index 1649400..cb511b7 100644 --- a/tests/test_sms_fallback.py +++ b/tests/test_sms_fallback.py @@ -92,3 +92,51 @@ def test_send_code_backup_also_fails_raises_backup_error(monkeypatch): with pytest.raises(SmsError) as ei: sms.send_code(PHONE) assert "创蓝" in str(ei.value) + + +def test_verify_hits_primary_without_touching_backup(monkeypatch): + calls = [] + monkeypatch.setattr(settings, "SMS_PROVIDER", "jiguang") + monkeypatch.setattr(settings, "SMS_FALLBACK_PROVIDER", "chuanglan") + monkeypatch.setattr(jiguang, "verify_code", + lambda p, c: (calls.append("jiguang"), True)[1]) + monkeypatch.setattr(chuanglan, "verify_code", + lambda p, c: (calls.append("chuanglan"), True)[1]) + + assert sms.verify_code(PHONE, "123456") is True + assert calls == ["jiguang"] + + +def test_verify_falls_through_to_backup(monkeypatch): + calls = [] + monkeypatch.setattr(settings, "SMS_PROVIDER", "jiguang") + monkeypatch.setattr(settings, "SMS_FALLBACK_PROVIDER", "chuanglan") + monkeypatch.setattr(jiguang, "verify_code", + lambda p, c: (calls.append("jiguang"), False)[1]) + monkeypatch.setattr(chuanglan, "verify_code", + lambda p, c: (calls.append("chuanglan"), True)[1]) + + assert sms.verify_code(PHONE, "123456") is True + assert calls == ["jiguang", "chuanglan"] + + +def test_verify_all_miss_returns_false(monkeypatch): + monkeypatch.setattr(settings, "SMS_PROVIDER", "jiguang") + monkeypatch.setattr(settings, "SMS_FALLBACK_PROVIDER", "chuanglan") + monkeypatch.setattr(jiguang, "verify_code", lambda p, c: False) + monkeypatch.setattr(chuanglan, "verify_code", lambda p, c: False) + + assert sms.verify_code(PHONE, "123456") is False + + +def test_verify_backup_not_touched_when_fallback_off(monkeypatch): + calls = [] + monkeypatch.setattr(settings, "SMS_PROVIDER", "jiguang") + monkeypatch.setattr(settings, "SMS_FALLBACK_PROVIDER", "") + monkeypatch.setattr(jiguang, "verify_code", + lambda p, c: (calls.append("jiguang"), False)[1]) + monkeypatch.setattr(chuanglan, "verify_code", + lambda p, c: (calls.append("chuanglan"), True)[1]) + + assert sms.verify_code(PHONE, "123456") is False + assert calls == ["jiguang"]