Files
bid-doctor/server/tests/test_auth.py
YANG JIANKUAN ad7fba6021 feat(server): 工程骨架后端——上传 → 解析 → 清单最小链路
- core:配置(prod 拒绝默认密钥 / dev 短信 / 假模型)、租户事务 + 行级安全策略、
  problem+json 错误码、信封加密与签名下载、对象存储、事务内入队、Valkey stream 事件流、权限点
- identity:手机号 HMAC 存储、注册、登录(挤下线需确认,API3)、服务端会话
- billing:额度流水只追加,预扣 / 结算 / 退回,一笔预扣只有一个结局
- document:分片上传、按文件头识别格式、Gotenberg 转换、文字层与稳定句子编号
- checklist:标注规则识别、模型抽取、三道校验、标注类条款规则兜底、重跑对齐
- ai:提示词注册表(extract.substantive v1 来自 S4 v2)、假模型、OpenAI 兼容供应商、ai_call 流水
- 迁移:data-model 基线、套餐、Procrastinate、会话状态函数
- 测试 30 个:租户隔离、额度、失败退回、对象加密、契约一致性
- Compose(含 migrate)、镜像、CI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 18:48:10 +08:00

114 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import httpx
from tests.conftest import User, register
async def test_register_creates_tenant_plan_and_quota(user: User) -> None:
me = (await user.get("/me")).json()
assert me["tenant"]["name"] == "测试集成公司"
assert me["role"] == "owner" and "project.create" in me["permissions"]
assert me["plan"]["code"] == "free"
assert me["user"]["phone_hint"] == f"{user.phone[:3]}****{user.phone[-4:]}"
assert [(a["unit"], a["balance"]) for a in (await user.get("/quota")).json()["accounts"]] == [("parse", 3)]
async def test_phone_is_not_stored_in_plaintext(user: User) -> None:
from sqlalchemy import text
from bid_doctor.core.db import anonymous_tx
async with anonymous_tx() as s:
rows = (await s.execute(text("SELECT external_user_id, contact_hint FROM login_identity"))).all()
assert all(user.phone not in (ext, hint) for ext, hint in rows)
async def test_login_unregistered_keeps_code_for_register(app_client: httpx.AsyncClient) -> None:
phone = "13700000001"
await app_client.post("/auth/sms-codes", json={"phone": phone, "purpose": "login"})
r = await app_client.post("/auth/login", json={"phone": phone, "sms_code": "000000", "client_kind": "web"})
assert r.status_code == 404 and r.json()["code"] == "phone_not_registered"
r = await app_client.post(
"/auth/register",
json={
"phone": phone,
"sms_code": "000000",
"tenant_name": "乙公司",
"display_name": "李工",
"terms_version": "v1",
"client_kind": "web",
},
)
assert r.status_code == 201, r.text
async def test_wrong_code_is_rejected(app_client: httpx.AsyncClient) -> None:
phone = "13700000002"
await app_client.post("/auth/sms-codes", json={"phone": phone, "purpose": "register"})
r = await app_client.post(
"/auth/register",
json={
"phone": phone,
"sms_code": "123456",
"tenant_name": "丙公司",
"display_name": "王工",
"terms_version": "v1",
"client_kind": "web",
},
)
assert r.status_code == 422 and r.json()["code"] == "sms_code_invalid"
async def test_concurrent_login_needs_confirmation_then_kicks_old_device(app_client: httpx.AsyncClient) -> None:
u = await register(app_client)
from bid_doctor.core.valkey import valkey
await valkey().flushdb() # 跳过 60 秒重发间隔
await app_client.post("/auth/sms-codes", json={"phone": u.phone, "purpose": "login"})
body = {"phone": u.phone, "sms_code": "000000", "client_kind": "desktop"}
r = await app_client.post("/auth/login", json=body)
assert r.status_code == 409 and r.json()["code"] == "concurrent_session"
assert len(r.json()["sessions"]) == 1
r = await app_client.post("/auth/login", json={**body, "force": True})
assert r.status_code == 200
old = await u.get("/me") # 旧设备被挤下线(API3),而且知道是为什么
assert old.status_code == 401
assert old.json()["code"] == "session_revoked"
assert old.json()["reason"] == "concurrent_login"
new = {"Authorization": f"Bearer {r.json()['token']}"}
assert (await app_client.get("/me", headers=new)).status_code == 200
async def test_web_cookie_requires_csrf_header_on_writes(app_client: httpx.AsyncClient) -> None:
phone = "13700000003"
await app_client.post("/auth/sms-codes", json={"phone": phone, "purpose": "register"})
r = await app_client.post(
"/auth/register",
json={
"phone": phone,
"sms_code": "000000",
"tenant_name": "丁公司",
"display_name": "赵工",
"terms_version": "v1",
"client_kind": "web",
},
)
assert "token" not in r.json()
cookies = {"bd_session": r.cookies["bd_session"], "bd_csrf": r.cookies["bd_csrf"]}
app_client.cookies.clear() # 共用的测试客户端不留 cookie,后面的用例要看到「未登录」
jar = httpx.Cookies(cookies)
assert (await app_client.get("/me", cookies=jar)).status_code == 200
r = await app_client.post("/projects", json={}, cookies=jar)
assert r.status_code == 403 and r.json()["code"] == "csrf_failed"
r = await app_client.post("/projects", json={}, cookies=jar, headers={"X-CSRF-Token": cookies["bd_csrf"]})
assert r.status_code == 201
async def test_unauthenticated_and_problem_format(app_client: httpx.AsyncClient) -> None:
r = await app_client.get("/me")
assert r.status_code == 401
assert r.headers["content-type"].startswith("application/problem+json")
body = r.json()
assert body["code"] == "unauthenticated" and body["request_id"] == r.headers["x-request-id"]