fix(m0): 迁移可执行 + 测试全绿(33 条)
CI / backend (push) Canceled after 0s

跑通迁移与测试后修掉的真问题:

- alembic 0001:asyncpg 走预备语句,一次 execute 只能一条 SQL。
  CREATE FUNCTION / CREATE POLICY / GRANT 全部拆成单语句逐条执行,
  否则报 "cannot insert multiple commands into a prepared statement"。
- 创建工作区:ORM flush 只按 relationship 排序,裸外键不算数,
  member 可能先于 workspace 落地撞 FK。改为先 flush workspace 再插 member。
- datetime.UTC 是 3.11+ 才有的,本机 3.10 直接 ImportError。改用 timezone.utc。
- 幂等中间件:原来会缓存 4xx。校验失败被缓存后,用户改对参数仍拿到同一个 422。
  改为只缓存 <400。
- 发码节流窗口改为可配置 SMS_THROTTLE_SECONDS(测试置 0),
  节流用例自己显式打开,避免"整库关掉节流"导致节流从未被验证。
- 租户隔离用例:pytest.raises(Exception) 改为精确的 ProgrammingError,
  否则任何异常都会让"应用角色不能建表"这条断言假通过。

验收:pytest 33 passed;ruff check / format 通过;真机跑 uvicorn,
/health、/version、登录、建工作区、列表、/auth/me 全通。
This commit is contained in:
Steven
2026-10-28 03:22:59 +08:00
parent 0c53aa7062
commit 2b493bd9f8
14 changed files with 160 additions and 136 deletions
+11 -16
View File
@@ -22,26 +22,25 @@ async def test_login_is_idempotent_for_same_phone(client: AsyncClient) -> None:
async def test_wrong_code_rejected(client: AsyncClient) -> None:
await client.post("/api/v1/auth/sms/send", json={"phone": "13800000003"})
resp = await client.post(
"/api/v1/auth/login", json={"phone": "13800000003", "code": "000000"}
)
resp = await client.post("/api/v1/auth/login", json={"phone": "13800000003", "code": "000000"})
assert resp.status_code == 400
assert resp.json()["code"] == 2010
async def test_code_can_only_be_used_once(client: AsyncClient) -> None:
await client.post("/api/v1/auth/sms/send", json={"phone": "13800000004"})
ok = await client.post(
"/api/v1/auth/login", json={"phone": "13800000004", "code": DEV_CODE}
)
ok = await client.post("/api/v1/auth/login", json={"phone": "13800000004", "code": DEV_CODE})
assert ok.status_code == 200
again = await client.post(
"/api/v1/auth/login", json={"phone": "13800000004", "code": DEV_CODE}
)
again = await client.post("/api/v1/auth/login", json={"phone": "13800000004", "code": DEV_CODE})
assert again.json()["code"] == 2010
async def test_sms_throttled(client: AsyncClient) -> None:
async def test_sms_throttled(client: AsyncClient, monkeypatch) -> None:
# 全局测试默认关掉了节流(SMS_THROTTLE_SECONDS=0),这里显式打开
from app.config import get_settings
monkeypatch.setattr(get_settings(), "sms_throttle_seconds", 60)
payload = {"phone": "13800000005"}
assert (await client.post("/api/v1/auth/sms/send", json=payload)).status_code == 200
second = await client.post("/api/v1/auth/sms/send", json=payload)
@@ -57,9 +56,7 @@ async def test_refresh_token_cannot_be_used_as_access_token(client: AsyncClient)
async def test_access_token_cannot_be_used_to_refresh(client: AsyncClient) -> None:
user = await login(client)
resp = await client.post(
"/api/v1/auth/refresh", json={"refresh_token": user["token"]}
)
resp = await client.post("/api/v1/auth/refresh", json={"refresh_token": user["token"]})
assert resp.status_code == 401
assert resp.json()["code"] == 2003
@@ -67,9 +64,7 @@ async def test_access_token_cannot_be_used_to_refresh(client: AsyncClient) -> No
async def test_refresh_returns_working_token_pair(client: AsyncClient) -> None:
user = await login(client)
data = unwrap(
await client.post(
"/api/v1/auth/refresh", json={"refresh_token": user["refresh_token"]}
)
await client.post("/api/v1/auth/refresh", json={"refresh_token": user["refresh_token"]})
)
me = await client.get("/api/v1/auth/me", headers=auth(data["access_token"]))
assert me.status_code == 200