feat(auth): 测试账号免验证码登录 + 每次走引导 + 每日上限 (#69)
Co-authored-by: zzhyyyyy <2685922758@qq.com> Reviewed-on: #69 Co-authored-by: zhuzihao <zhuzihao@wonderable.ai> Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #69.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""测试账号(app/core/test_account.py)行为测试。
|
||||
|
||||
覆盖四件事:
|
||||
- 免验证码登录(mock 与 real 模式都跳过校验);
|
||||
- 每次登录都重走新手引导(onboarding_completed 恒 false,即便完成表已有记录);
|
||||
- 每日登录上限(超过即 429),且只算 login、不算 send;
|
||||
- 功能关闭(TEST_ACCOUNT_PHONE 留空)时对任何号都不生效,回到原逻辑。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core import test_account
|
||||
from app.integrations import sms
|
||||
|
||||
# 测试自注入的固定号:各用例用 monkeypatch 把它塞进 settings.TEST_ACCOUNT_PHONE,
|
||||
# 再断言行为。它与 .env 配的值【相互独立】——测试不读 .env(保证确定性 / CI 无 .env 也能跑),
|
||||
# 写成和 .env 同值只为直观;换任意 11 位号测试照样全过,二者无需保持一致。
|
||||
TEST_PHONE = "11111111111"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def enabled(monkeypatch):
|
||||
"""启用测试账号功能并清空当日计数;monkeypatch 与 _reset 保证用例间隔离。"""
|
||||
monkeypatch.setattr(test_account.settings, "TEST_ACCOUNT_PHONE", TEST_PHONE)
|
||||
test_account._reset_for_test()
|
||||
yield
|
||||
test_account._reset_for_test()
|
||||
|
||||
|
||||
# ============================ 开关 / 识别 ============================
|
||||
|
||||
def test_disabled_when_phone_unset(monkeypatch) -> None:
|
||||
"""TEST_ACCOUNT_PHONE 留空 = 整功能关闭,对测试号也不特殊处理。"""
|
||||
monkeypatch.setattr(test_account.settings, "TEST_ACCOUNT_PHONE", "")
|
||||
assert test_account.is_enabled() is False
|
||||
assert test_account.is_test_account(TEST_PHONE) is False
|
||||
|
||||
|
||||
def test_only_configured_phone_is_test_account(enabled) -> None:
|
||||
assert test_account.is_test_account(TEST_PHONE) is True
|
||||
assert test_account.is_test_account("13800138000") is False
|
||||
|
||||
|
||||
def test_phone_is_whitespace_trimmed(monkeypatch) -> None:
|
||||
""".env 里手机号前后带空格也能正确识别。"""
|
||||
monkeypatch.setattr(test_account.settings, "TEST_ACCOUNT_PHONE", f" {TEST_PHONE} ")
|
||||
assert test_account.is_test_account(TEST_PHONE) is True
|
||||
|
||||
|
||||
# ============================ 免验证码 + 新手引导 ============================
|
||||
|
||||
def test_login_skips_code_and_returns_tokens(client, enabled) -> None:
|
||||
"""测试号任意验证码即可登入(不用等真短信),拿到 JWT,onboarding 为 false。"""
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/login",
|
||||
json={"phone": TEST_PHONE, "code": "000000", "device_id": "dev-a"},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
data = r.json()
|
||||
assert data["user"]["phone"] == TEST_PHONE
|
||||
assert data["user"]["register_channel"] == "sms"
|
||||
assert data["onboarding_completed"] is False
|
||||
assert data["force_onboarding"] is True # 测试号:应用内重登也强制重走引导
|
||||
assert "access_token" in data and "refresh_token" in data
|
||||
|
||||
|
||||
def test_onboarding_always_false_even_if_completed(client, enabled) -> None:
|
||||
"""即便 onboarding_completion 表已有 (该用户, 该设备) 记录,测试号再登仍返回 false。"""
|
||||
from app.db.session import SessionLocal
|
||||
from app.repositories import onboarding as onboarding_repo
|
||||
|
||||
device = "dev-onb"
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/login",
|
||||
json={"phone": TEST_PHONE, "code": "123456", "device_id": device},
|
||||
)
|
||||
uid = r.json()["user"]["id"]
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
onboarding_repo.mark_completed(db, user_id=uid, device_id=device)
|
||||
assert onboarding_repo.is_completed(db, user_id=uid, device_id=device) is True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
r2 = client.post(
|
||||
"/api/v1/auth/sms/login",
|
||||
json={"phone": TEST_PHONE, "code": "123456", "device_id": device},
|
||||
)
|
||||
assert r2.json()["onboarding_completed"] is False # 仍重走引导
|
||||
|
||||
|
||||
# ============================ real 模式跳过 ============================
|
||||
|
||||
def test_login_skips_verify_in_real_mode(client, enabled, monkeypatch) -> None:
|
||||
"""real 模式(SMS_MOCK=false)普通号要真校验;测试号必须跳过 → 任意码仍 200。"""
|
||||
monkeypatch.setattr(sms.settings, "SMS_MOCK", False)
|
||||
r = client.post(
|
||||
"/api/v1/auth/sms/login",
|
||||
json={"phone": TEST_PHONE, "code": "9999"},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
|
||||
def test_send_short_circuits_in_real_mode(client, enabled, monkeypatch) -> None:
|
||||
"""real 模式下测试号 /sms/send 不真打极光(否则缺号/缺 key 会 503),直接 200 放行进填码界面。"""
|
||||
monkeypatch.setattr(sms.settings, "SMS_MOCK", False)
|
||||
r = client.post("/api/v1/auth/sms/send", json={"phone": TEST_PHONE})
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["sent"] is True
|
||||
assert body["cooldown_sec"] == 0
|
||||
|
||||
|
||||
# ============================ 每日上限 ============================
|
||||
|
||||
def test_daily_limit_blocks_after_threshold(client, enabled, monkeypatch) -> None:
|
||||
"""当日登录数到上限后再登 → 429。"""
|
||||
monkeypatch.setattr(test_account.settings, "TEST_ACCOUNT_DAILY_LIMIT", 2)
|
||||
test_account._reset_for_test()
|
||||
|
||||
for _ in range(2):
|
||||
r = client.post("/api/v1/auth/sms/login", json={"phone": TEST_PHONE, "code": "123456"})
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
r = client.post("/api/v1/auth/sms/login", json={"phone": TEST_PHONE, "code": "123456"})
|
||||
assert r.status_code == 429
|
||||
assert "上限" in r.json()["detail"]
|
||||
|
||||
|
||||
def test_send_does_not_consume_quota(client, enabled, monkeypatch) -> None:
|
||||
"""额度只算 login、不算 send:狂发 send 后那唯一一次 login 仍能成功。"""
|
||||
monkeypatch.setattr(test_account.settings, "TEST_ACCOUNT_DAILY_LIMIT", 1)
|
||||
test_account._reset_for_test()
|
||||
|
||||
for _ in range(3):
|
||||
assert client.post("/api/v1/auth/sms/send", json={"phone": TEST_PHONE}).status_code == 200
|
||||
|
||||
assert client.post("/api/v1/auth/sms/login", json={"phone": TEST_PHONE, "code": "1234"}).status_code == 200
|
||||
# 第二次 login 才超限
|
||||
assert client.post("/api/v1/auth/sms/login", json={"phone": TEST_PHONE, "code": "1234"}).status_code == 429
|
||||
|
||||
|
||||
# ============================ 计数单元逻辑 ============================
|
||||
|
||||
def test_quota_resets_across_day(enabled, monkeypatch) -> None:
|
||||
"""跨天自动归零。"""
|
||||
monkeypatch.setattr(test_account.settings, "TEST_ACCOUNT_DAILY_LIMIT", 1)
|
||||
monkeypatch.setattr(test_account, "_today", lambda: "2026-06-23")
|
||||
assert test_account.try_consume_quota() is True
|
||||
assert test_account.try_consume_quota() is False # 当天超限
|
||||
|
||||
monkeypatch.setattr(test_account, "_today", lambda: "2026-06-24")
|
||||
assert test_account.try_consume_quota() is True # 次日归零放行
|
||||
|
||||
|
||||
def test_non_test_phone_unaffected_when_enabled(client, enabled) -> None:
|
||||
"""功能开启时,普通号走原 mock 流程(任意 6 位通过),不受测试号逻辑影响。"""
|
||||
phone = "13812341234"
|
||||
client.post("/api/v1/auth/sms/send", json={"phone": phone})
|
||||
r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456", "device_id": "d1"})
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["user"]["phone"] == phone
|
||||
assert r.json()["force_onboarding"] is False # 普通号不强制重走,按原逻辑
|
||||
Reference in New Issue
Block a user