219 lines
7.9 KiB
Python
219 lines
7.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
流程测试:文章编辑 @某人 不存在时自动创建 Person + 存客宝获客计划
|
||
|
||
需求来源:临时需求池/2026-03-16-文章编辑自动创建@和#.md
|
||
验收:编辑文章输入 @新人物(链接人与事中无)→ 保存 → 链接人与事列表出现「新人物」,存客宝有对应计划
|
||
|
||
流程:管理端 ensureMentionsAndTags 对 content 中 @name 调用 POST /api/db/persons {name}
|
||
→ 后端按 name 查找,不存在则创建 Person + 调用存客宝创建获客计划
|
||
|
||
前置条件:存客宝 API 可连通(CKB_OPEN_API_KEY 等配置正确),且存在名为 soul 的设备;否则创建新 Person 会失败
|
||
"""
|
||
import random
|
||
import time
|
||
|
||
import pytest
|
||
import requests
|
||
|
||
from util import admin_headers
|
||
|
||
|
||
def _unique_name():
|
||
"""生成唯一名称,避免与已有 Person 冲突"""
|
||
return f"测试自动创建_{int(time.time() * 1000)}"
|
||
|
||
|
||
def test_person_ensure_creates_ckb_plan_when_not_exists(admin_token, base_url):
|
||
"""
|
||
@某人 不存在时:POST /api/db/persons 仅传 name → 应创建 Person 并自动创建存客宝获客计划
|
||
"""
|
||
if not admin_token:
|
||
pytest.skip("admin 登录失败,跳过")
|
||
name = _unique_name()
|
||
r = requests.post(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
json={"name": name},
|
||
timeout=15,
|
||
)
|
||
assert r.status_code == 200, f"响应: {r.text}"
|
||
data = r.json()
|
||
assert data.get("success") is True, f"success 应为 true: {data}"
|
||
person = data.get("person")
|
||
assert person is not None, "应返回 person"
|
||
assert person.get("name") == name
|
||
assert person.get("personId"), "应有 personId"
|
||
assert person.get("token"), "应有 token(小程序 @ 点击时兑换密钥)"
|
||
# 存客宝获客计划应已创建
|
||
ckb_plan_id = person.get("ckbPlanId") or 0
|
||
assert ckb_plan_id > 0, f"应自动创建存客宝计划,ckbPlanId 应 > 0,实际: {ckb_plan_id}"
|
||
assert person.get("ckbApiKey"), "应有 ckbApiKey"
|
||
|
||
|
||
def test_person_ensure_returns_existing_when_name_exists(admin_token, base_url):
|
||
"""
|
||
@某人 已存在时:POST /api/db/persons 仅传 name → 应返回已有 Person,不重复创建
|
||
"""
|
||
if not admin_token:
|
||
pytest.skip("admin 登录失败,跳过")
|
||
name = _unique_name()
|
||
# 第一次创建
|
||
r1 = requests.post(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
json={"name": name},
|
||
timeout=15,
|
||
)
|
||
assert r1.status_code == 200 and r1.json().get("success")
|
||
first_id = r1.json()["person"]["personId"]
|
||
# 第二次相同 name,应返回已有
|
||
r2 = requests.post(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
json={"name": name},
|
||
timeout=15,
|
||
)
|
||
assert r2.status_code == 200 and r2.json().get("success")
|
||
second = r2.json()["person"]
|
||
assert second["personId"] == first_id, "相同 name 应返回同一 Person"
|
||
|
||
|
||
def test_person_ensure_rejects_empty_name(admin_token, base_url):
|
||
"""name 为空时 POST /api/db/persons 应返回错误(不依赖存客宝)"""
|
||
if not admin_token:
|
||
pytest.skip("admin 登录失败,跳过")
|
||
r = requests.post(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
json={"name": ""},
|
||
timeout=10,
|
||
)
|
||
assert r.status_code == 200
|
||
data = r.json()
|
||
assert data.get("success") is False
|
||
assert "name" in (data.get("error") or "").lower() or "必填" in (data.get("error") or "")
|
||
|
||
|
||
def test_article_mention_flow_persons_list_contains_new(admin_token, base_url):
|
||
"""
|
||
流程:创建新 Person 后,GET /api/db/persons 列表应包含该人
|
||
"""
|
||
if not admin_token:
|
||
pytest.skip("admin 登录失败,跳过")
|
||
name = _unique_name()
|
||
r_create = requests.post(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
json={"name": name},
|
||
timeout=15,
|
||
)
|
||
assert r_create.status_code == 200 and r_create.json().get("success")
|
||
person_id = r_create.json()["person"]["personId"]
|
||
# 拉列表
|
||
r_list = requests.get(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
timeout=10,
|
||
)
|
||
assert r_list.status_code == 200 and r_list.json().get("success")
|
||
persons = r_list.json().get("persons") or []
|
||
found = [p for p in persons if p.get("personId") == person_id]
|
||
assert len(found) == 1, f"列表应包含新建的 Person {person_id}"
|
||
assert found[0].get("ckbPlanId", 0) > 0, "列表中应有 ckbPlanId"
|
||
|
||
|
||
def test_new_article_save_auto_creates_person_and_ckb(admin_token, base_url):
|
||
"""
|
||
完整流程:新建文章,content 含 @新人物 → 保存时 ensureMentionsAndTags 自动 POST persons
|
||
→ 创建 Person + 存客宝获客计划 → 再 PUT book 保存文章
|
||
"""
|
||
if not admin_token:
|
||
pytest.skip("admin 登录失败,跳过")
|
||
ts = int(time.time() * 1000)
|
||
rnd = random.randint(100000, 999999)
|
||
name = f"测试新人物_{ts}_{rnd}"
|
||
# chapters.id 限制 size:20,用短 id
|
||
section_id = f"t{rnd}"
|
||
|
||
# 1. 获取 book 结构,取第一个 part/chapter
|
||
r_list = requests.get(
|
||
f"{base_url}/api/db/book?action=list",
|
||
headers=admin_headers(admin_token),
|
||
timeout=10,
|
||
)
|
||
assert r_list.status_code == 200 and r_list.json().get("success")
|
||
sections = r_list.json().get("sections") or []
|
||
part_id = "part-1"
|
||
chapter_id = "chapter-1"
|
||
part_title = "未分类"
|
||
chapter_title = "未分类"
|
||
if sections:
|
||
first = sections[0]
|
||
part_id = first.get("partId") or part_id
|
||
chapter_id = first.get("chapterId") or chapter_id
|
||
part_title = first.get("partTitle") or part_title
|
||
chapter_title = first.get("chapterTitle") or chapter_title
|
||
|
||
# 2. 模拟 ensureMentionsAndTags:content 含 @name 时先 POST persons
|
||
content = f"这是一篇测试文章,@{name} 会被自动创建并同步存客宝。"
|
||
r_person = requests.post(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
json={"name": name},
|
||
timeout=15,
|
||
)
|
||
assert r_person.status_code == 200, f"创建 Person 失败: {r_person.text}"
|
||
person_data = r_person.json()
|
||
assert person_data.get("success") is True, f"Person 创建失败: {person_data}"
|
||
person = person_data.get("person")
|
||
assert person and person.get("ckbPlanId", 0) > 0, "应自动创建存客宝获客计划"
|
||
|
||
# 3. 新建文章(PUT /api/db/book)
|
||
payload = {
|
||
"id": section_id,
|
||
"title": f"测试自动创建_{ts}",
|
||
"content": content,
|
||
"price": 1,
|
||
"isFree": False,
|
||
"partId": part_id,
|
||
"partTitle": part_title,
|
||
"chapterId": chapter_id,
|
||
"chapterTitle": chapter_title,
|
||
"editionStandard": True,
|
||
"editionPremium": False,
|
||
"isNew": False,
|
||
"hotScore": 0,
|
||
}
|
||
r_put = requests.put(
|
||
f"{base_url}/api/db/book",
|
||
headers=admin_headers(admin_token),
|
||
json=payload,
|
||
timeout=15,
|
||
)
|
||
assert r_put.status_code == 200, f"保存文章失败: {r_put.text}"
|
||
put_data = r_put.json()
|
||
assert put_data.get("success") is True, f"保存文章失败: {put_data}"
|
||
|
||
# 4. 验证 persons 列表包含新人物
|
||
r_persons = requests.get(
|
||
f"{base_url}/api/db/persons",
|
||
headers=admin_headers(admin_token),
|
||
timeout=10,
|
||
)
|
||
assert r_persons.status_code == 200
|
||
persons = r_persons.json().get("persons") or []
|
||
found = [p for p in persons if p.get("name") == name]
|
||
assert len(found) == 1, f"链接人与事列表应包含「{name}」"
|
||
assert found[0].get("ckbPlanId", 0) > 0, "应有存客宝获客计划"
|
||
|
||
# 5. 清理:删除测试文章(避免重复运行冲突)
|
||
try:
|
||
requests.delete(
|
||
f"{base_url}/api/db/book?id={section_id}",
|
||
headers=admin_headers(admin_token),
|
||
timeout=10,
|
||
)
|
||
except Exception:
|
||
pass
|