test_knowledge_api.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. from datetime import UTC, datetime
  2. from fastapi.testclient import TestClient
  3. from zbt.core.config import Settings
  4. from zbt.core.passwords import PasswordService
  5. from zbt.domains.identity.models import AdminUser
  6. from zbt.domains.identity.repository import InMemoryIdentityRepository
  7. from zbt.domains.knowledge.index import InMemoryKnowledgeIndex
  8. from zbt.domains.knowledge.repository import InMemoryKnowledgeRepository
  9. from zbt.main import create_app
  10. def test_admin_creates_draft_knowledge_document() -> None:
  11. settings = Settings(
  12. app_env="test",
  13. jwt_access_secret="a" * 32,
  14. jwt_refresh_secret="b" * 32,
  15. field_encryption_key="c" * 32,
  16. )
  17. identities = InMemoryIdentityRepository()
  18. identities.save_admin_user(
  19. AdminUser(
  20. id="01ADMIN0000000000000000001",
  21. username="admin",
  22. password_hash=PasswordService().hash("zaq1XSW@"),
  23. display_name="知识管理员",
  24. status="ACTIVE",
  25. roles=("SUPER_ADMIN",),
  26. permissions=("knowledge:read", "knowledge:write"),
  27. data_scope="ALL",
  28. )
  29. )
  30. knowledge = InMemoryKnowledgeRepository()
  31. application = create_app(
  32. settings=settings,
  33. identity_repository=identities,
  34. knowledge_repository=knowledge,
  35. clock=lambda: datetime(2026, 7, 27, 2, 0, tzinfo=UTC),
  36. )
  37. with TestClient(application) as client:
  38. login = client.post(
  39. "/api/v1/admin/auth/login",
  40. json={"username": "admin", "password": "zaq1XSW@"},
  41. )
  42. token = login.json()["data"]["tokens"]["access_token"]
  43. response = client.post(
  44. "/api/v1/admin/knowledge/documents",
  45. headers={"Authorization": f"Bearer {token}"},
  46. json={
  47. "title": "安心医疗险保险条款",
  48. "document_type": "INSURANCE_TERMS",
  49. "source_name": "安心医疗险条款.pdf",
  50. "product_code": "MED-BASIC",
  51. "content": "本产品等待期为30天,意外伤害导致的医疗费用不受等待期限制。",
  52. },
  53. )
  54. assert response.status_code == 201
  55. document = response.json()["data"]
  56. assert document["title"] == "安心医疗险保险条款"
  57. assert document["document_type"] == "INSURANCE_TERMS"
  58. assert document["source_name"] == "安心医疗险条款.pdf"
  59. assert document["product_code"] == "MED-BASIC"
  60. assert document["status"] == "DRAFT"
  61. assert document["version_no"] == 1
  62. assert document["chunk_count"] == 0
  63. assert document["indexed_at"] is None
  64. assert document["published_at"] is None
  65. def test_admin_cannot_publish_document_before_indexing() -> None:
  66. settings = Settings(
  67. app_env="test",
  68. jwt_access_secret="a" * 32,
  69. jwt_refresh_secret="b" * 32,
  70. field_encryption_key="c" * 32,
  71. )
  72. identities = InMemoryIdentityRepository()
  73. identities.save_admin_user(
  74. AdminUser(
  75. id="01ADMIN0000000000000000001",
  76. username="admin",
  77. password_hash=PasswordService().hash("zaq1XSW@"),
  78. display_name="知识管理员",
  79. status="ACTIVE",
  80. roles=("SUPER_ADMIN",),
  81. permissions=("knowledge:read", "knowledge:write", "knowledge:publish"),
  82. data_scope="ALL",
  83. )
  84. )
  85. application = create_app(
  86. settings=settings,
  87. identity_repository=identities,
  88. knowledge_repository=InMemoryKnowledgeRepository(),
  89. clock=lambda: datetime(2026, 7, 27, 2, 0, tzinfo=UTC),
  90. )
  91. with TestClient(application) as client:
  92. login = client.post(
  93. "/api/v1/admin/auth/login",
  94. json={"username": "admin", "password": "zaq1XSW@"},
  95. )
  96. headers = {
  97. "Authorization": f"Bearer {login.json()['data']['tokens']['access_token']}"
  98. }
  99. created = client.post(
  100. "/api/v1/admin/knowledge/documents",
  101. headers=headers,
  102. json={
  103. "title": "安心医疗险保险条款",
  104. "document_type": "INSURANCE_TERMS",
  105. "source_name": "安心医疗险条款.pdf",
  106. "content": "本产品等待期为30天,意外伤害导致的医疗费用不受等待期限制。",
  107. },
  108. )
  109. document_id = created.json()["data"]["document_id"]
  110. response = client.post(
  111. f"/api/v1/admin/knowledge/documents/{document_id}/publish",
  112. headers=headers,
  113. )
  114. assert response.status_code == 409
  115. assert response.json()["error"]["code"] == "KNOWLEDGE_DOCUMENT_NOT_INDEXED"
  116. def test_admin_cannot_publish_indexed_document_without_effective_search_test() -> None:
  117. settings = Settings(
  118. app_env="test",
  119. jwt_access_secret="a" * 32,
  120. jwt_refresh_secret="b" * 32,
  121. field_encryption_key="c" * 32,
  122. )
  123. identities = InMemoryIdentityRepository()
  124. identities.save_admin_user(
  125. AdminUser(
  126. id="01ADMIN0000000000000000001",
  127. username="admin",
  128. password_hash=PasswordService().hash("zaq1XSW@"),
  129. display_name="知识管理员",
  130. status="ACTIVE",
  131. roles=("SUPER_ADMIN",),
  132. permissions=("knowledge:read", "knowledge:write", "knowledge:publish"),
  133. data_scope="ALL",
  134. )
  135. )
  136. application = create_app(
  137. settings=settings,
  138. identity_repository=identities,
  139. knowledge_repository=InMemoryKnowledgeRepository(),
  140. knowledge_index=InMemoryKnowledgeIndex(),
  141. clock=lambda: datetime(2026, 7, 27, 2, 0, tzinfo=UTC),
  142. )
  143. with TestClient(application) as client:
  144. login = client.post(
  145. "/api/v1/admin/auth/login",
  146. json={"username": "admin", "password": "zaq1XSW@"},
  147. )
  148. headers = {
  149. "Authorization": f"Bearer {login.json()['data']['tokens']['access_token']}"
  150. }
  151. created = client.post(
  152. "/api/v1/admin/knowledge/documents",
  153. headers=headers,
  154. json={
  155. "title": "安心医疗险保险条款",
  156. "document_type": "INSURANCE_TERMS",
  157. "source_name": "安心医疗险条款.pdf",
  158. "content": "本产品等待期为30天,意外伤害不受等待期限制。",
  159. },
  160. )
  161. document_id = created.json()["data"]["document_id"]
  162. client.post(
  163. f"/api/v1/admin/knowledge/documents/{document_id}/index",
  164. headers=headers,
  165. )
  166. response = client.post(
  167. f"/api/v1/admin/knowledge/documents/{document_id}/publish",
  168. headers=headers,
  169. )
  170. assert response.status_code == 409
  171. assert response.json()["error"]["code"] == "KNOWLEDGE_SEARCH_TEST_REQUIRED"
  172. def test_admin_indexes_then_publishes_document() -> None:
  173. settings = Settings(
  174. app_env="test",
  175. jwt_access_secret="a" * 32,
  176. jwt_refresh_secret="b" * 32,
  177. field_encryption_key="c" * 32,
  178. )
  179. identities = InMemoryIdentityRepository()
  180. identities.save_admin_user(
  181. AdminUser(
  182. id="01ADMIN0000000000000000001",
  183. username="admin",
  184. password_hash=PasswordService().hash("zaq1XSW@"),
  185. display_name="知识管理员",
  186. status="ACTIVE",
  187. roles=("SUPER_ADMIN",),
  188. permissions=("knowledge:read", "knowledge:write", "knowledge:publish"),
  189. data_scope="ALL",
  190. )
  191. )
  192. application = create_app(
  193. settings=settings,
  194. identity_repository=identities,
  195. knowledge_repository=InMemoryKnowledgeRepository(),
  196. knowledge_index=InMemoryKnowledgeIndex(),
  197. clock=lambda: datetime(2026, 7, 27, 2, 0, tzinfo=UTC),
  198. )
  199. with TestClient(application) as client:
  200. login = client.post(
  201. "/api/v1/admin/auth/login",
  202. json={"username": "admin", "password": "zaq1XSW@"},
  203. )
  204. headers = {
  205. "Authorization": f"Bearer {login.json()['data']['tokens']['access_token']}"
  206. }
  207. created = client.post(
  208. "/api/v1/admin/knowledge/documents",
  209. headers=headers,
  210. json={
  211. "title": "安心医疗险保险条款",
  212. "document_type": "INSURANCE_TERMS",
  213. "source_name": "安心医疗险条款.pdf",
  214. "content": (
  215. "等待期规则\n本产品疾病医疗等待期为30天,意外伤害不受等待期限制。\n\n"
  216. "报销材料\n申请医疗费用报销协助时,需要提供发票、费用清单和诊断证明。"
  217. ),
  218. },
  219. )
  220. document_id = created.json()["data"]["document_id"]
  221. indexed = client.post(
  222. f"/api/v1/admin/knowledge/documents/{document_id}/index",
  223. headers=headers,
  224. )
  225. search_test = client.post(
  226. f"/api/v1/admin/knowledge/documents/{document_id}/search-test",
  227. headers=headers,
  228. json={"query": "等待期是多少天?", "limit": 3},
  229. )
  230. published = client.post(
  231. f"/api/v1/admin/knowledge/documents/{document_id}/publish",
  232. headers=headers,
  233. )
  234. search_tests = client.get(
  235. f"/api/v1/admin/knowledge/documents/{document_id}/search-tests",
  236. headers=headers,
  237. )
  238. assert indexed.status_code == 200
  239. assert indexed.json()["data"]["status"] == "INDEXED"
  240. assert indexed.json()["data"]["chunk_count"] == 2
  241. assert indexed.json()["data"]["indexed_at"] is not None
  242. assert search_test.status_code == 200
  243. assert search_test.json()["data"]["test_record"]["passed"] is True
  244. assert search_test.json()["data"]["test_record"]["hit_count"] >= 1
  245. assert published.status_code == 200
  246. assert published.json()["data"]["status"] == "PUBLISHED"
  247. assert published.json()["data"]["published_at"] is not None
  248. assert published.json()["data"]["search_test_count"] == 1
  249. assert published.json()["data"]["last_search_test_passed"] is True
  250. assert search_tests.status_code == 200
  251. assert search_tests.json()["data"]["total"] == 1
  252. assert search_tests.json()["data"]["items"][0]["query"] == "等待期是多少天?"
  253. def test_admin_lists_and_disables_published_document() -> None:
  254. settings = Settings(
  255. app_env="test",
  256. jwt_access_secret="a" * 32,
  257. jwt_refresh_secret="b" * 32,
  258. field_encryption_key="c" * 32,
  259. )
  260. identities = InMemoryIdentityRepository()
  261. identities.save_admin_user(
  262. AdminUser(
  263. id="01ADMIN0000000000000000001",
  264. username="admin",
  265. password_hash=PasswordService().hash("zaq1XSW@"),
  266. display_name="知识管理员",
  267. status="ACTIVE",
  268. roles=("SUPER_ADMIN",),
  269. permissions=("knowledge:read", "knowledge:write", "knowledge:publish"),
  270. data_scope="ALL",
  271. )
  272. )
  273. application = create_app(
  274. settings=settings,
  275. identity_repository=identities,
  276. knowledge_repository=InMemoryKnowledgeRepository(),
  277. knowledge_index=InMemoryKnowledgeIndex(),
  278. clock=lambda: datetime(2026, 7, 27, 2, 0, tzinfo=UTC),
  279. )
  280. with TestClient(application) as client:
  281. login = client.post(
  282. "/api/v1/admin/auth/login",
  283. json={"username": "admin", "password": "zaq1XSW@"},
  284. )
  285. headers = {
  286. "Authorization": f"Bearer {login.json()['data']['tokens']['access_token']}"
  287. }
  288. created = client.post(
  289. "/api/v1/admin/knowledge/documents",
  290. headers=headers,
  291. json={
  292. "title": "安心医疗险保险条款",
  293. "document_type": "INSURANCE_TERMS",
  294. "source_name": "安心医疗险条款.pdf",
  295. "content": "本产品疾病医疗等待期为30天,意外伤害不受等待期限制。",
  296. },
  297. )
  298. document_id = created.json()["data"]["document_id"]
  299. client.post(
  300. f"/api/v1/admin/knowledge/documents/{document_id}/index",
  301. headers=headers,
  302. )
  303. client.post(
  304. f"/api/v1/admin/knowledge/documents/{document_id}/search-test",
  305. headers=headers,
  306. json={"query": "等待期是多少天?"},
  307. )
  308. client.post(
  309. f"/api/v1/admin/knowledge/documents/{document_id}/publish",
  310. headers=headers,
  311. )
  312. disabled = client.post(
  313. f"/api/v1/admin/knowledge/documents/{document_id}/disable",
  314. headers=headers,
  315. )
  316. listing = client.get("/api/v1/admin/knowledge/documents", headers=headers)
  317. assert disabled.status_code == 200
  318. assert disabled.json()["data"]["status"] == "INACTIVE"
  319. assert listing.status_code == 200
  320. assert listing.json()["data"]["total"] == 1
  321. assert listing.json()["data"]["items"][0]["status"] == "INACTIVE"
  322. def test_admin_uploads_utf8_text_as_draft_document() -> None:
  323. settings = Settings(
  324. app_env="test",
  325. jwt_access_secret="a" * 32,
  326. jwt_refresh_secret="b" * 32,
  327. field_encryption_key="c" * 32,
  328. )
  329. identities = InMemoryIdentityRepository()
  330. identities.save_admin_user(
  331. AdminUser(
  332. id="01ADMIN0000000000000000001",
  333. username="admin",
  334. password_hash=PasswordService().hash("zaq1XSW@"),
  335. display_name="知识管理员",
  336. status="ACTIVE",
  337. roles=("SUPER_ADMIN",),
  338. permissions=("knowledge:read", "knowledge:write"),
  339. data_scope="ALL",
  340. )
  341. )
  342. application = create_app(
  343. settings=settings,
  344. identity_repository=identities,
  345. knowledge_repository=InMemoryKnowledgeRepository(),
  346. knowledge_index=InMemoryKnowledgeIndex(),
  347. clock=lambda: datetime(2026, 7, 27, 2, 0, tzinfo=UTC),
  348. )
  349. with TestClient(application) as client:
  350. login = client.post(
  351. "/api/v1/admin/auth/login",
  352. json={"username": "admin", "password": "zaq1XSW@"},
  353. )
  354. headers = {
  355. "Authorization": f"Bearer {login.json()['data']['tokens']['access_token']}"
  356. }
  357. response = client.post(
  358. "/api/v1/admin/knowledge/documents/upload",
  359. headers=headers,
  360. data={
  361. "title": "医疗费用报销材料说明",
  362. "document_type": "SERVICE_RULES",
  363. "product_code": "MED-BASIC",
  364. },
  365. files={
  366. "file": (
  367. "医疗费用报销材料说明.txt",
  368. "申请时需要提供发票、费用清单和诊断证明。".encode(),
  369. "text/plain",
  370. )
  371. },
  372. )
  373. assert response.status_code == 201
  374. document = response.json()["data"]
  375. assert document["title"] == "医疗费用报销材料说明"
  376. assert document["source_name"] == "医疗费用报销材料说明.txt"
  377. assert document["document_type"] == "SERVICE_RULES"
  378. assert document["status"] == "DRAFT"
  379. def test_admin_tests_indexed_document_before_publishing() -> None:
  380. settings = Settings(
  381. app_env="test",
  382. jwt_access_secret="a" * 32,
  383. jwt_refresh_secret="b" * 32,
  384. field_encryption_key="c" * 32,
  385. )
  386. identities = InMemoryIdentityRepository()
  387. identities.save_admin_user(
  388. AdminUser(
  389. id="01ADMIN0000000000000000001",
  390. username="admin",
  391. password_hash=PasswordService().hash("zaq1XSW@"),
  392. display_name="知识管理员",
  393. status="ACTIVE",
  394. roles=("SUPER_ADMIN",),
  395. permissions=("knowledge:read", "knowledge:write"),
  396. data_scope="ALL",
  397. )
  398. )
  399. application = create_app(
  400. settings=settings,
  401. identity_repository=identities,
  402. knowledge_repository=InMemoryKnowledgeRepository(),
  403. knowledge_index=InMemoryKnowledgeIndex(),
  404. clock=lambda: datetime(2026, 7, 27, 2, 0, tzinfo=UTC),
  405. )
  406. with TestClient(application) as client:
  407. login = client.post(
  408. "/api/v1/admin/auth/login",
  409. json={"username": "admin", "password": "zaq1XSW@"},
  410. )
  411. headers = {
  412. "Authorization": f"Bearer {login.json()['data']['tokens']['access_token']}"
  413. }
  414. created = client.post(
  415. "/api/v1/admin/knowledge/documents",
  416. headers=headers,
  417. json={
  418. "title": "安心医疗险保险条款",
  419. "document_type": "INSURANCE_TERMS",
  420. "source_name": "安心医疗险条款.md",
  421. "content": "疾病医疗等待期为30天,意外伤害不受等待期限制。",
  422. },
  423. )
  424. document_id = created.json()["data"]["document_id"]
  425. client.post(
  426. f"/api/v1/admin/knowledge/documents/{document_id}/index",
  427. headers=headers,
  428. )
  429. response = client.post(
  430. f"/api/v1/admin/knowledge/documents/{document_id}/search-test",
  431. headers=headers,
  432. json={"query": "等待期是多少天?", "limit": 3},
  433. )
  434. assert response.status_code == 200
  435. result = response.json()["data"]
  436. assert result["document_id"] == document_id
  437. assert result["total"] == 1
  438. assert "30天" in result["items"][0]["content"]