Files
server-core/tests/test_auth.py
T
Pine 26fa546f67 feat: 核心服务端基础框架(身份/平台 API)
- 七端口 RBAC、select-identity、JWT、审计
- FastAPI + SQLAlchemy + SQLite,/auth /opc /admin /agents 等路由
2026-08-23 22:35:59 +08:00

246 lines
8.1 KiB
Python

# -*- coding: utf-8 -*-
"""认证接口端到端测试(使用临时 SQLite 数据库,不污染 data/)。"""
from __future__ import annotations
DEMO_USERNAME = "pine"
DEMO_PASSWORD = "123456"
def login(client, username=DEMO_USERNAME, password=DEMO_PASSWORD) -> dict:
res = client.post("/auth/login", json={"username": username, "password": password})
assert res.status_code == 200, res.text
return res.json()
def auth(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
# ---------------------------------------------------------------------------
# 基础 / 状态
# ---------------------------------------------------------------------------
def test_health(client):
res = client.get("/health")
assert res.status_code == 200
assert res.json()["status"] == "ok"
def test_status(client):
res = client.get("/auth/status")
assert res.status_code == 200
assert res.json() == {"enabled": True, "has_users": True}
# ---------------------------------------------------------------------------
# 登录 / 注册
# ---------------------------------------------------------------------------
def test_login_success_returns_profile_and_token(client):
data = login(client)
assert data["token"]
assert data["username"] == DEMO_USERNAME
# 演示资料字段齐全
for field in ("nickname", "account", "company", "room", "avatar", "company_avatar"):
assert field in data
def test_login_wrong_password(client):
res = client.post(
"/auth/login",
json={"username": DEMO_USERNAME, "password": "wrong"},
)
assert res.status_code == 401
assert res.json()["detail"] == "Invalid username or password"
def test_register_denied_when_users_exist(client):
res = client.post(
"/auth/register",
json={"username": "13800001234", "password": "whatever1"},
)
assert res.status_code == 403
assert res.json()["detail"] == "User already registered"
# ---------------------------------------------------------------------------
# 令牌校验 / 当前用户
# ---------------------------------------------------------------------------
def test_verify_with_token(client):
token = login(client)["token"]
res = client.get("/auth/verify", headers=auth(token))
assert res.status_code == 200
assert res.json() == {"valid": True, "username": DEMO_USERNAME}
def test_verify_without_token(client):
res = client.get("/auth/verify")
assert res.status_code == 401
def test_me_returns_profile(client):
token = login(client)["token"]
res = client.get("/auth/me", headers=auth(token))
assert res.status_code == 200
assert res.json()["username"] == DEMO_USERNAME
assert res.json()["nickname"]
# ---------------------------------------------------------------------------
# 资料 / 凭据更新
# ---------------------------------------------------------------------------
def test_update_profile_fields(client):
token = login(client)["token"]
res = client.post(
"/auth/update-profile",
headers=auth(token),
json={"nickname": "新昵称", "account": "新账号", "room": "2001"},
)
assert res.status_code == 200, res.text
body = res.json()
assert body["nickname"] == "新昵称"
assert body["account"] == "新账号"
assert body["room"] == "2001"
# 仅改资料不重签令牌
assert body["token"] == ""
def test_update_profile_nothing_to_update(client):
token = login(client)["token"]
res = client.post(
"/auth/update-profile",
headers=auth(token),
json={"current_password": DEMO_PASSWORD},
)
assert res.status_code == 400
assert res.json()["detail"] == "Nothing to update"
def test_update_profile_wrong_current_password(client):
token = login(client)["token"]
res = client.post(
"/auth/update-profile",
headers=auth(token),
json={"current_password": "wrong", "new_password": "newpass123"},
)
assert res.status_code == 401
assert res.json()["detail"] == "Current password is incorrect"
def test_update_profile_changes_password_and_reissues_token(client):
token = login(client)["token"]
res = client.post(
"/auth/update-profile",
headers=auth(token),
json={"current_password": DEMO_PASSWORD, "new_password": "newpass123"},
)
assert res.status_code == 200, res.text
body = res.json()
assert body["token"] # 新令牌
# 旧令牌失效,新令牌可用
assert client.get("/auth/verify", headers=auth(token)).status_code == 401
assert (
client.get("/auth/verify", headers=auth(body["token"])).status_code == 200
)
# 新密码可登录,旧密码不可
assert login(client, password="newpass123")["token"]
assert (
client.post(
"/auth/login",
json={"username": DEMO_USERNAME, "password": DEMO_PASSWORD},
).status_code
== 401
)
def test_update_profile_changes_username(client):
token = login(client)["token"]
res = client.post(
"/auth/update-profile",
headers=auth(token),
json={"current_password": DEMO_PASSWORD, "new_username": "pine2"},
)
assert res.status_code == 200, res.text
assert res.json()["username"] == "pine2"
assert res.json()["token"]
# 新用户名可登录
res = client.post(
"/auth/login",
json={"username": "pine2", "password": DEMO_PASSWORD},
)
assert res.status_code == 200
def test_update_profile_empty_new_password_rejected(client):
token = login(client)["token"]
res = client.post(
"/auth/update-profile",
headers=auth(token),
json={"current_password": DEMO_PASSWORD, "new_password": " "},
)
assert res.status_code == 400
assert "cannot be empty" in res.json()["detail"]
# ---------------------------------------------------------------------------
# 令牌吊销
# ---------------------------------------------------------------------------
def test_revoke_current_token(client):
token = login(client)["token"]
res = client.post("/auth/revoke-token", headers=auth(token), json={})
assert res.status_code == 200
assert res.json()["revoked"] is True
assert res.json()["revoked_current_token"] is True
assert client.get("/auth/verify", headers=auth(token)).status_code == 401
def test_revoke_specific_token(client):
token_a = login(client)["token"]
token_b = login(client)["token"]
res = client.post(
"/auth/revoke-token",
headers=auth(token_a),
json={"token": token_b},
)
assert res.status_code == 200
assert res.json()["revoked_current_token"] is False
assert client.get("/auth/verify", headers=auth(token_b)).status_code == 401
assert client.get("/auth/verify", headers=auth(token_a)).status_code == 200
def test_revoke_all_tokens(client):
token_a = login(client)["token"]
token_b = login(client)["token"]
res = client.post("/auth/revoke-all-tokens", headers=auth(token_a), json={})
assert res.status_code == 200
assert res.json()["revoked"] is True
assert client.get("/auth/verify", headers=auth(token_a)).status_code == 401
assert client.get("/auth/verify", headers=auth(token_b)).status_code == 401
# ---------------------------------------------------------------------------
# 落盘格式
# ---------------------------------------------------------------------------
def test_persisted_user_has_no_plaintext_password(client, tmp_path):
login(client)
# 从 SQLite 读取用户记录,断言不落明文密码
import sqlite3
conn = sqlite3.connect(tmp_path / "test.db")
row = conn.execute(
"SELECT username, password_hash, password_salt FROM users WHERE username=?",
(DEMO_USERNAME,),
).fetchone()
conn.close()
assert row is not None
username, password_hash, password_salt = row
assert username == DEMO_USERNAME
assert password_hash
assert password_salt
# users 表没有 password 明文列
conn = sqlite3.connect(tmp_path / "test.db")
cols = [r[1] for r in conn.execute("PRAGMA table_info(users)")]
conn.close()
assert "password" not in cols