feat: 多租户账号体系 + 前台收藏按钮
- 新增 users 表(user_id 数据隔离,bcrypt 密码) - 认证: 注册/登录(用户名+密码)/会话绑定用户, 首个用户成为管理员并接管旧数据 - 数据隔离: 笔记/分类/标签/回收站/版本/草稿/图谱/FTS 全部按用户隔离 - 前台: 登录/注册弹窗, 登录后★收藏自己的笔记, 游客只读公开笔记 - 后台: 用户名+密码登录, 每人管理自己的工作区, 越权访问返回404 - 冒烟测试重构+新增多租户隔离用例(78/78)
This commit is contained in:
+74
-15
@@ -55,7 +55,7 @@ def req(method, path, data=None, raw=False, headers=None):
|
||||
except Exception as ex:
|
||||
return -1, {"error": str(ex)}
|
||||
|
||||
print("═══ 1. 认证安全 ═══")
|
||||
print("═══ 1. 认证安全(多租户用户名+密码) ═══")
|
||||
|
||||
# 未登录访问管理 API -> 401
|
||||
st, d = req("GET", "/admin/api/tree")
|
||||
@@ -68,23 +68,38 @@ st, d = req("POST", "/admin/notes")
|
||||
report("未登录(错误路径)不泄露", st in (401, 404), f"(got {st})")
|
||||
|
||||
# 错误密码登录
|
||||
st, d = req("POST", "/admin/login", "password=wrongpass")
|
||||
st, d = req("POST", "/admin/login", "username=smoketest&password=wrongpass")
|
||||
report("错误密码登录返回401", st == 401, f"(got {st})")
|
||||
|
||||
# 正确登录
|
||||
st, d = req("POST", "/admin/login", "password=admin123")
|
||||
report("正确密码登录成功", st == 200 and d.get("code") == 0, f"(got {st} {d})")
|
||||
# 用随机用户名注册一个专属测试管理员(保证可重复运行)
|
||||
import uuid as _uuid
|
||||
TUSER = "smoke_" + _uuid.uuid4().hex[:8]
|
||||
st, d = req("POST", "/api/auth/register", {"username": TUSER, "password": "smoke123", "display_name": "冒烟测试"})
|
||||
report("注册测试用户成功", st == 200 and d.get("code") == 0, f"(got {st} {d})")
|
||||
|
||||
# 注册后自动登录 -> 可访问管理 API
|
||||
st, d = req("GET", "/admin/api/tree")
|
||||
report("注册后自动登录(管理API可用)", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
|
||||
# 退出登录
|
||||
st, d = req("POST", "/api/auth/logout")
|
||||
report("登出成功", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
|
||||
# 用户名+密码正确登录
|
||||
st, d = req("POST", "/api/auth/login", {"username": TUSER, "password": "smoke123"})
|
||||
report("用户名密码登录成功", st == 200 and d.get("code") == 0, f"(got {st} {d})")
|
||||
|
||||
# 登录后可访问
|
||||
st, d = req("GET", "/admin/api/tree")
|
||||
report("登录后访问管理API成功", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
|
||||
# 重复注册同一用户名 -> 失败
|
||||
st, d = req("POST", "/api/auth/register", {"username": TUSER, "password": "smoke123"})
|
||||
report("重复注册同名用户被拒", st == 400, f"(got {st} {d})")
|
||||
|
||||
# 旧固定 cookie 不再有效
|
||||
class TmpOpener:
|
||||
pass
|
||||
# 手动构造请求带旧的 fixed cookie
|
||||
import urllib.request as u
|
||||
reqf = urllib.request.Request(BASE + "/admin/api/tree", headers={"Cookie": "admin_token=authenticated"})
|
||||
reqf = urllib.request.Request(BASE + "/admin/api/tree", headers={"Cookie": "note_token=authenticated"})
|
||||
try:
|
||||
r = u.urlopen(reqf, timeout=10)
|
||||
st_old = r.status
|
||||
@@ -124,21 +139,24 @@ st, d = req("POST", "/admin/api/notes", {
|
||||
protected_id = d.get("data", {}).get("id")
|
||||
report("创建带密码笔记成功", st == 200 and protected_id, f"(got {st})")
|
||||
|
||||
# 通过公开接口 GET /api/notes/:id 访问 -> 应被拒
|
||||
# 登出后(游客身份)通过公开接口 GET /api/notes/:id 访问 -> 应被拒
|
||||
req("POST", "/api/auth/logout")
|
||||
st, d = req("GET", f"/api/notes/{protected_id}")
|
||||
report("公开接口拒绝带密码笔记", st in (401, 403), f"(got {st})")
|
||||
report("游客公开接口拒绝带密码笔记", st in (401, 403), f"(got {st})")
|
||||
|
||||
# 密码验证接口应能正确返回
|
||||
# 密码验证接口(公开,游客)应能正确返回
|
||||
st, d = req("POST", f"/api/notes/{protected_id}/access", {"password": "secret123"})
|
||||
report("密码验证访问成功", st == 200 and "秘密内容" in d.get("data", {}).get("content", ""), f"(got {st})")
|
||||
|
||||
st, d = req("POST", f"/api/notes/{protected_id}/access", {"password": "wrong"})
|
||||
report("错误密码访问被拒", st == 401, f"(got {st})")
|
||||
|
||||
# 公开无密码笔记
|
||||
public_ok = False
|
||||
# 公开无密码笔记(游客)
|
||||
st, d = req("GET", f"/api/notes/{note_id}")
|
||||
report("公开接口读取无密码笔记成功", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
report("游客公开接口读取无密码笔记成功", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
|
||||
# 重新登录回测试用户
|
||||
req("POST", "/api/auth/login", {"username": TUSER, "password": "smoke123"})
|
||||
|
||||
print("═══ 4. 回收站(软删除/恢复/清空) ═══")
|
||||
|
||||
@@ -341,6 +359,47 @@ report("合并后标签去重", "已重命名" in tag_str and "保留" not in ta
|
||||
st, d = req("DELETE", "/admin/api/tags", {"name": "已重命名"})
|
||||
report("删除标签", st == 200, f"(got {st})")
|
||||
|
||||
# ── 多租户数据隔离 ──
|
||||
print("\n═══ 12b. 多租户数据隔离 ═══")
|
||||
# 记住当前测试用户可见的笔记(应只有本脚本创建的 + 自己空间的)
|
||||
st, d = req("GET", "/admin/api/tree")
|
||||
own_ids = {x["id"] for x in d.get("data", [])}
|
||||
|
||||
# 注册第二个用户
|
||||
st, d = req("POST", "/api/auth/register", {"username": "iso_" + _uuid.uuid4().hex[:8], "password": "iso12345"})
|
||||
report("第二个用户注册成功", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
|
||||
# 第二个用户的树必须是空的(看不到第一个用户的笔记)
|
||||
st, d = req("GET", "/admin/api/tree")
|
||||
other_ids = {x["id"] for x in d.get("data", [])}
|
||||
report("第二用户树为空(数据隔离)", st == 200 and len(other_ids) == 0, f"(got {other_ids})")
|
||||
|
||||
# 两用户笔记 ID 无交集
|
||||
report("两用户数据无交集", len(own_ids & other_ids) == 0, f"(own={own_ids} other={other_ids})")
|
||||
|
||||
# 第二用户无权读取第一用户创建的笔记
|
||||
st, d = req("GET", f"/admin/api/notes/{note_id}")
|
||||
report("越权读取他人笔记被拒(404)", st == 404, f"(got {st})")
|
||||
|
||||
# 第二用户无权收藏/删除他人笔记
|
||||
st, d = req("PUT", f"/admin/api/notes/{note_id}", {"is_favorite": True})
|
||||
report("越权修改他人笔记被拒(404)", st == 404, f"(got {st})")
|
||||
|
||||
# 第二用户 FTS 搜索不到第一用户的笔记
|
||||
q = urllib.parse.urlencode({"q": "测试笔记A"})
|
||||
st, d = req("GET", f"/api/notes/search?{q}")
|
||||
other_hits = d.get("data") or []
|
||||
report("第二用户搜不到他人笔记", len(other_hits) == 0, f"(got {len(other_hits)})")
|
||||
|
||||
# 游客公开接口能读取公开笔记(跨租户展示,需登出为游客)
|
||||
req("POST", "/api/auth/logout")
|
||||
st, d = req("GET", f"/api/notes/{note_id}")
|
||||
report("游客可读公开笔记", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
|
||||
# 重新登录回第一个测试用户(后续清理需要)
|
||||
st, d = req("POST", "/api/auth/login", {"username": TUSER, "password": "smoke123"})
|
||||
report("切回测试用户成功", st == 200 and d.get("code") == 0, f"(got {st})")
|
||||
|
||||
# ── 清理测试数据(彻底删除本脚本创建的所有笔记及其版本)──
|
||||
print("\n═══ 13. 清理测试数据 ═══")
|
||||
test_ids = [x for x in (note_id, protected_id, tmp_id, fts_id, linkA_id, linkB_id) if x]
|
||||
|
||||
Reference in New Issue
Block a user