feat(m0): 后端骨架 + Web/安卓前端骨架 + 设计文档

- backend: FastAPI 应用工厂、统一错误信封、JWT、短信登录、工作区/成员/邀请、幂等中间件
- alembic 0001: workspace/member/invitation/audit_log/app_user + RLS 策略 + 双角色 GRANT
- tests: 认证/工作区/租户隔离/幂等 四组用例(尚未在本机执行)
- web: React18+Vite 骨架(登录、工作区、邀请落地页)
- android: Compose 骨架(登录、工作区列表)
- 2.Design: PRODUCT-PLAN / ROADMAP-M0-M1 / NAMING / DEVELOPMENT
This commit is contained in:
Steven
2026-10-28 02:38:53 +08:00
commit 0c53aa7062
72 changed files with 5320 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
"""认证相关。"""
from __future__ import annotations
from httpx import AsyncClient
from .conftest import DEV_CODE, auth, login, unwrap
async def test_login_creates_user_and_me_works(client: AsyncClient) -> None:
user = await login(client, "13800000001")
me = unwrap(await client.get("/api/v1/auth/me", headers=user["headers"]))
assert me["user"]["phone"] == "138****0001" # 手机号对外必须打码
assert me["workspaces"] == []
async def test_login_is_idempotent_for_same_phone(client: AsyncClient) -> None:
first = await login(client, "13800000002")
second = await login(client, "13800000002")
assert first["user_id"] == second["user_id"]
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"}
)
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}
)
assert ok.status_code == 200
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:
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)
assert second.json()["code"] == 2011
async def test_refresh_token_cannot_be_used_as_access_token(client: AsyncClient) -> None:
user = await login(client)
resp = await client.get("/api/v1/auth/me", headers=auth(user["refresh_token"]))
assert resp.status_code == 401
assert resp.json()["code"] == 2003
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"]}
)
assert resp.status_code == 401
assert resp.json()["code"] == 2003
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"]}
)
)
me = await client.get("/api/v1/auth/me", headers=auth(data["access_token"]))
assert me.status_code == 200
async def test_requires_token(client: AsyncClient) -> None:
resp = await client.get("/api/v1/auth/me")
assert resp.status_code == 401
assert resp.json()["code"] == 2001
assert resp.json()["request_id"] != "-"
async def test_unknown_path_returns_envelope(client: AsyncClient) -> None:
resp = await client.get("/api/v1/nope")
assert resp.status_code == 404
assert set(resp.json()) == {"code", "message", "data", "request_id"}