conftest.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. """B1测试公共配置。"""
  2. from __future__ import annotations
  3. import sys
  4. import os
  5. import tempfile
  6. from pathlib import Path
  7. import pytest
  8. BACKEND_ROOT = Path(__file__).resolve().parents[2]
  9. if str(BACKEND_ROOT) not in sys.path:
  10. sys.path.insert(0, str(BACKEND_ROOT))
  11. @pytest.fixture(scope="session")
  12. def flask_app():
  13. # 本组用例不真正连库,仅需 SQLAlchemy 可解析的方言;达梦 dmPython
  14. # 属部署期驱动(测试环境未安装),默认 DM 链接会导致引擎创建失败,
  15. # 故未显式配置 DMS_DATABASE_URL 时固定注入 MySQL 占位链接。
  16. os.environ.setdefault(
  17. "DMS_DATABASE_URL",
  18. "mysql+pymysql://dms_app:change-me@127.0.0.1:3306/dms?charset=utf8mb4",
  19. )
  20. from app import app
  21. app.config.update(TESTING=True)
  22. return app
  23. @pytest.fixture()
  24. def client(flask_app):
  25. return flask_app.test_client()
  26. @pytest.fixture()
  27. def b2_app():
  28. database_url = os.environ.get("DMS_TEST_DATABASE_URL")
  29. jwt_secret = os.environ.get("DMS_TEST_JWT_SECRET")
  30. password = os.environ.get("DMS_TEST_USER_PASSWORD")
  31. if not database_url or not database_url.rsplit("/", 1)[-1].split("?", 1)[0].endswith("_test"):
  32. pytest.skip("DMS_TEST_DATABASE_URL必须指向以_test结尾的独立测试库")
  33. if not jwt_secret or not password:
  34. pytest.skip("缺少B2测试专用密钥或密码环境变量")
  35. from flask import Flask
  36. from flask_cors import CORS
  37. from sqlalchemy import delete, update
  38. from dms import init_dms
  39. from dms.extensions import db
  40. from dms.models import (
  41. AttachmentBinding,
  42. AuditLog,
  43. Category,
  44. Document,
  45. DocumentRelation,
  46. Organization,
  47. Permission,
  48. Role,
  49. RolePermission,
  50. SubPlanBinding,
  51. User,
  52. )
  53. from dms.security.passwords import hash_password
  54. test_storage = tempfile.TemporaryDirectory(prefix="dms-test-storage-")
  55. os.environ["DMS_DATABASE_URL"] = database_url
  56. os.environ["DMS_JWT_SECRET"] = jwt_secret
  57. os.environ["DMS_STORAGE_ROOT"] = test_storage.name
  58. app = Flask("dms-b2-test")
  59. CORS(app)
  60. init_dms(app)
  61. app.config.update(TESTING=True)
  62. with app.app_context():
  63. db.session.execute(delete(AuditLog))
  64. # 角色两表:先解除用户 role_id 引用,避免外键残留(角色测试
  65. # 结束后 role_cleanup 无 teardown,残行会卡死机构清理)
  66. db.session.execute(update(User).values(role_id=None))
  67. db.session.execute(delete(RolePermission))
  68. db.session.execute(delete(Role))
  69. db.session.execute(delete(SubPlanBinding))
  70. db.session.execute(delete(DocumentRelation))
  71. db.session.execute(delete(Permission))
  72. db.session.execute(delete(AttachmentBinding))
  73. db.session.execute(
  74. delete(Document).where(Document.document_type == "SUB_PLAN")
  75. )
  76. db.session.execute(delete(Document))
  77. db.session.execute(update(Category).values(parent_id=None))
  78. db.session.execute(delete(Category))
  79. db.session.execute(delete(User))
  80. db.session.execute(update(Organization).values(parent_id=None))
  81. db.session.execute(delete(Organization))
  82. db.session.commit()
  83. root = Organization(
  84. org_code="ORG_ROOT",
  85. org_name="机关",
  86. org_path="/机关",
  87. sort_no=10,
  88. status="ENABLED",
  89. )
  90. db.session.add(root)
  91. db.session.flush()
  92. ops = Organization(
  93. org_code="ORG_OPS",
  94. org_name="作战部",
  95. org_path="/机关/作战部",
  96. parent_id=root.id,
  97. sort_no=20,
  98. status="ENABLED",
  99. )
  100. comms = Organization(
  101. org_code="ORG_COMMS",
  102. org_name="通信部",
  103. org_path="/机关/通信部",
  104. parent_id=root.id,
  105. sort_no=30,
  106. status="ENABLED",
  107. )
  108. disabled_org = Organization(
  109. org_code="ORG_DISABLED",
  110. org_name="停用部门",
  111. org_path="/机关/停用部门",
  112. parent_id=root.id,
  113. sort_no=40,
  114. status="DISABLED",
  115. )
  116. db.session.add_all([ops, comms, disabled_org])
  117. db.session.flush()
  118. db.session.add_all(
  119. [
  120. User(
  121. username="admin",
  122. password_hash=hash_password(password),
  123. real_name="系统管理员",
  124. organization_id=root.id,
  125. organization_name=root.org_name,
  126. role_code="ADMIN",
  127. security_level="TOP_SECRET",
  128. status="ENABLED",
  129. ),
  130. User(
  131. username="auditor",
  132. password_hash=hash_password(password),
  133. real_name="历史审计员",
  134. organization_id=root.id,
  135. organization_name=root.org_name,
  136. role_code="USER",
  137. security_level="TOP_SECRET",
  138. status="DISABLED",
  139. auth_version=1,
  140. is_deleted=True,
  141. ),
  142. User(
  143. username="user",
  144. password_hash=hash_password(password),
  145. real_name="普通用户",
  146. organization_id=ops.id,
  147. organization_name=ops.org_name,
  148. role_code="USER",
  149. security_level="SECRET",
  150. status="ENABLED",
  151. ),
  152. User(
  153. username="disabled",
  154. password_hash=hash_password(password),
  155. real_name="停用用户",
  156. organization_id=comms.id,
  157. organization_name=comms.org_name,
  158. role_code="USER",
  159. security_level="SECRET",
  160. status="DISABLED",
  161. ),
  162. ]
  163. )
  164. db.session.commit()
  165. yield app
  166. with app.app_context():
  167. db.session.remove()
  168. test_storage.cleanup()
  169. @pytest.fixture()
  170. def b2_client(b2_app):
  171. return b2_app.test_client()
  172. @pytest.fixture()
  173. def b2_password():
  174. password = os.environ.get("DMS_TEST_USER_PASSWORD")
  175. if not password:
  176. pytest.skip("缺少B2测试专用密码环境变量DMS_TEST_USER_PASSWORD")
  177. return password
  178. @pytest.fixture()
  179. def login_token(b2_client, b2_password):
  180. response = b2_client.post(
  181. "/api/v1/auth/login",
  182. json={
  183. "username": "admin",
  184. "password": b2_password,
  185. "keepSignedIn": False,
  186. },
  187. )
  188. assert response.status_code == 200
  189. return response.get_json()["data"]["accessToken"]
  190. @pytest.fixture()
  191. def token_for(b2_client, b2_password):
  192. def issue(username: str) -> str:
  193. response = b2_client.post(
  194. "/api/v1/auth/login",
  195. json={
  196. "username": username,
  197. "password": b2_password,
  198. "keepSignedIn": False,
  199. },
  200. )
  201. assert response.status_code == 200
  202. return response.get_json()["data"]["accessToken"]
  203. return issue
  204. @pytest.fixture()
  205. def role_cleanup(b2_app):
  206. """清空角色两表并解除用户角色关联,供角色管理/矩阵判定测试隔离。"""
  207. from sqlalchemy import delete, update
  208. from dms.extensions import db
  209. from dms.models import Role, RolePermission, User
  210. with b2_app.app_context():
  211. db.session.execute(update(User).values(role_id=None))
  212. db.session.execute(delete(RolePermission))
  213. db.session.execute(delete(Role))
  214. db.session.commit()
  215. @pytest.fixture()
  216. def b3_app(b2_app):
  217. from dms.extensions import db
  218. from dms.models import Category
  219. with b2_app.app_context():
  220. root_a = Category(
  221. category_code="ROOT_A",
  222. category_name="场景根A",
  223. category_type="SCENE",
  224. category_path="/场景根A",
  225. sort_no=10,
  226. document_count=0,
  227. status="ENABLED",
  228. )
  229. root_b = Category(
  230. category_code="ROOT_B",
  231. category_name="场景根B",
  232. category_type="SCENE",
  233. category_path="/场景根B",
  234. sort_no=10,
  235. document_count=0,
  236. status="ENABLED",
  237. )
  238. root_c = Category(
  239. category_code="ROOT_C",
  240. category_name="场景根C",
  241. category_type="SCENE",
  242. category_path="/场景根C",
  243. sort_no=30,
  244. document_count=0,
  245. status="ENABLED",
  246. )
  247. disabled = Category(
  248. category_code="DISABLED_ROOT",
  249. category_name="停用分类",
  250. category_type="OTHER",
  251. category_path="/停用分类",
  252. sort_no=40,
  253. document_count=0,
  254. status="DISABLED",
  255. )
  256. deleted = Category(
  257. category_code="DELETED_CODE",
  258. category_name="已删除分类",
  259. category_type="OTHER",
  260. category_path="/已删除分类",
  261. sort_no=50,
  262. document_count=0,
  263. status="ENABLED",
  264. is_deleted=True,
  265. )
  266. db.session.add_all([root_a, root_b, root_c, disabled, deleted])
  267. db.session.flush()
  268. db.session.add_all(
  269. [
  270. Category(
  271. category_code="CHILD_A1",
  272. category_name="样式A1",
  273. category_type="STYLE",
  274. parent_id=root_a.id,
  275. category_path="/场景根A/样式A1",
  276. sort_no=10,
  277. document_count=0,
  278. status="ENABLED",
  279. ),
  280. Category(
  281. category_code="CHILD_A2",
  282. category_name="样式A2",
  283. category_type="STYLE",
  284. parent_id=root_a.id,
  285. category_path="/场景根A/样式A2",
  286. sort_no=20,
  287. document_count=0,
  288. status="ENABLED",
  289. ),
  290. ]
  291. )
  292. db.session.commit()
  293. return b2_app
  294. @pytest.fixture()
  295. def b3_client(b3_app):
  296. return b3_app.test_client()
  297. @pytest.fixture()
  298. def b4_app(b3_app):
  299. from dms.extensions import db
  300. from dms.models import (
  301. AttachmentBinding,
  302. Category,
  303. Document,
  304. Organization,
  305. Permission,
  306. User,
  307. )
  308. def document(
  309. name: str,
  310. document_type: str,
  311. *,
  312. category: Category | None = None,
  313. status: str = "PUBLISHED",
  314. security: str = "INTERNAL",
  315. visibility: str = "ALL_AUTHENTICATED",
  316. parent: Document | None = None,
  317. root: Document | None = None,
  318. attachment_type: str | None = None,
  319. deleted: bool = False,
  320. ) -> Document:
  321. item = Document(
  322. document_name=name,
  323. summary=f"{name}摘要",
  324. document_type=document_type,
  325. document_status=status,
  326. security_level=security,
  327. visibility_type=visibility,
  328. attachment_type=attachment_type,
  329. category_id=category.id if category else None,
  330. category_name=category.category_name if category else None,
  331. category_path=category.category_path if category else None,
  332. parent_document_id=parent.id if parent else None,
  333. root_document_id=root.id if root else None,
  334. tags=["B4", "测试"],
  335. original_file_name=f"{name}.docx",
  336. file_relative_path=f"original/B4_TEST_{name}.docx",
  337. file_extension="docx",
  338. mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  339. file_size=128,
  340. file_hash="a" * 64,
  341. search_text=f"{name} B4 测试",
  342. created_by_name="系统管理员",
  343. updated_by_name="系统管理员",
  344. is_deleted=deleted,
  345. )
  346. db.session.add(item)
  347. db.session.flush()
  348. return item
  349. with b3_app.app_context():
  350. category = db.session.scalar(
  351. db.select(Category).where(Category.category_code == "ROOT_A")
  352. )
  353. admin = db.session.scalar(db.select(User).where(User.username == "admin"))
  354. user = db.session.scalar(db.select(User).where(User.username == "user"))
  355. root_org = db.session.scalar(
  356. db.select(Organization).where(Organization.org_code == "ORG_ROOT")
  357. )
  358. assert (
  359. category is not None
  360. and admin is not None
  361. and user is not None
  362. and root_org is not None
  363. )
  364. main_all = document("B4_TEST_全部主案", "MAIN", category=category)
  365. main_org = document(
  366. "B4_TEST_组织主案",
  367. "MAIN",
  368. category=category,
  369. security="SECRET",
  370. visibility="ORGANIZATION",
  371. )
  372. main_custom = document(
  373. "B4_TEST_自定义主案",
  374. "MAIN",
  375. category=category,
  376. security="SECRET",
  377. visibility="CUSTOM",
  378. )
  379. document(
  380. "B4_TEST_草稿主案",
  381. "MAIN",
  382. category=category,
  383. status="DRAFT",
  384. )
  385. document(
  386. "B4_TEST_绝密主案",
  387. "MAIN",
  388. category=category,
  389. security="TOP_SECRET",
  390. )
  391. document(
  392. "B4_TEST_无授权主案",
  393. "MAIN",
  394. category=category,
  395. visibility="CUSTOM",
  396. )
  397. sub = document(
  398. "B4_TEST_动态继承子案",
  399. "SUB_PLAN",
  400. category=category,
  401. parent=main_custom,
  402. root=main_custom,
  403. security="SECRET",
  404. # 故意与根主案不同,验证输出和授权动态继承根主案。
  405. visibility="ALL_AUTHENTICATED",
  406. )
  407. main_custom.child_count = 1
  408. attachment_one = document(
  409. "B4_TEST_规范附件",
  410. "ATTACHMENT",
  411. security="PUBLIC",
  412. visibility="ALL_AUTHENTICATED",
  413. attachment_type="TRAINING_MATERIAL",
  414. )
  415. attachment_two = document(
  416. "B4_TEST_表格附件",
  417. "ATTACHMENT",
  418. security="PUBLIC",
  419. visibility="ALL_AUTHENTICATED",
  420. attachment_type="SUPPORT_ACHIEVEMENT",
  421. )
  422. document(
  423. "B4_TEST_已删除附件",
  424. "ATTACHMENT",
  425. security="PUBLIC",
  426. visibility="ALL_AUTHENTICATED",
  427. attachment_type="TYPICAL_CASE",
  428. deleted=True,
  429. )
  430. db.session.add_all(
  431. [
  432. Permission(
  433. document_id=main_org.id,
  434. subject_type="ORG",
  435. subject_id=root_org.id,
  436. subject_name=root_org.org_name,
  437. can_view=True,
  438. can_download=True,
  439. created_by=admin.id,
  440. updated_by=admin.id,
  441. ),
  442. Permission(
  443. document_id=main_custom.id,
  444. subject_type="USER",
  445. subject_id=user.id,
  446. subject_name=user.real_name,
  447. can_view=True,
  448. can_download=True,
  449. created_by=admin.id,
  450. updated_by=admin.id,
  451. ),
  452. AttachmentBinding(
  453. main_document_id=main_all.id,
  454. attachment_document_id=attachment_one.id,
  455. sort_no=20,
  456. created_by=admin.id,
  457. updated_by=admin.id,
  458. ),
  459. AttachmentBinding(
  460. main_document_id=main_all.id,
  461. attachment_document_id=attachment_two.id,
  462. sort_no=10,
  463. created_by=admin.id,
  464. updated_by=admin.id,
  465. ),
  466. AttachmentBinding(
  467. main_document_id=main_org.id,
  468. attachment_document_id=attachment_one.id,
  469. sort_no=10,
  470. created_by=admin.id,
  471. updated_by=admin.id,
  472. ),
  473. ]
  474. )
  475. main_all.attachment_count = 2
  476. main_org.attachment_count = 1
  477. db.session.commit()
  478. b3_app.config["B4_IDS"] = {
  479. "main_all": main_all.id,
  480. "main_org": main_org.id,
  481. "main_custom": main_custom.id,
  482. "sub": sub.id,
  483. "attachment_one": attachment_one.id,
  484. "attachment_two": attachment_two.id,
  485. }
  486. return b3_app
  487. @pytest.fixture()
  488. def b4_client(b4_app):
  489. return b4_app.test_client()
  490. @pytest.fixture()
  491. def b5_app(b3_app):
  492. return b3_app
  493. @pytest.fixture()
  494. def b5_client(b5_app):
  495. return b5_app.test_client()
  496. @pytest.fixture()
  497. def b6_app(b4_app):
  498. return b4_app
  499. @pytest.fixture()
  500. def b6_client(b6_app):
  501. return b6_app.test_client()
  502. @pytest.fixture()
  503. def b7_app(b4_app):
  504. from datetime import datetime
  505. from dms.extensions import db
  506. from dms.models import AuditLog, User
  507. with b4_app.app_context():
  508. users = {
  509. item.username: item
  510. for item in db.session.scalars(db.select(User)).all()
  511. }
  512. def audit(
  513. username,
  514. created_at,
  515. action,
  516. *,
  517. result="SUCCESS",
  518. target_type="DOCUMENT",
  519. target_id=101,
  520. target_name="B7目标",
  521. real_name=None,
  522. organization_name=None,
  523. failure_reason=None,
  524. request_id=None,
  525. client_ip="127.0.0.1",
  526. ):
  527. user = users.get(username) if username else None
  528. row = AuditLog(
  529. user_id=user.id if user else None,
  530. username=username,
  531. real_name=real_name or (user.real_name if user else None),
  532. organization_id=user.organization_id if user else None,
  533. organization_name=(
  534. organization_name
  535. or (user.organization_name if user else None)
  536. ),
  537. action_type=action,
  538. target_type=target_type,
  539. target_id=target_id,
  540. target_name=target_name,
  541. operation_result=result,
  542. failure_reason=failure_reason,
  543. client_ip=client_ip,
  544. request_id=request_id,
  545. created_at=created_at,
  546. )
  547. db.session.add(row)
  548. db.session.flush()
  549. return row
  550. rows = [
  551. audit(
  552. "admin",
  553. datetime(2026, 1, 5, 0, 0),
  554. "VIEW_DOCUMENT",
  555. real_name="管理员旧名",
  556. organization_name="旧组织",
  557. target_name="关键目标",
  558. request_id="b7-request-alpha",
  559. ),
  560. audit(
  561. "admin",
  562. datetime(2026, 1, 5, 1, 0),
  563. "DOWNLOAD_DOCUMENT",
  564. result="FAILURE",
  565. failure_reason="关键失败原因",
  566. request_id="b7-request-beta",
  567. ),
  568. audit(
  569. "auditor",
  570. datetime(2026, 1, 6, 2, 0),
  571. "LOGIN",
  572. target_type="AUTH",
  573. target_id=users["auditor"].id,
  574. target_name="auditor",
  575. request_id="b7-request-gamma",
  576. ),
  577. audit(
  578. "user",
  579. datetime(2026, 1, 7, 0, 0),
  580. "VIEW_DOCUMENT",
  581. target_id=102,
  582. request_id="b7-request-delta",
  583. ),
  584. audit(
  585. "user",
  586. datetime(2026, 1, 7, 1, 0),
  587. "VIEW_DOCUMENT",
  588. target_id=103,
  589. request_id="b7-request-epsilon",
  590. ),
  591. audit(
  592. "admin",
  593. datetime(2026, 1, 7, 1, 0),
  594. "VIEW_DOCUMENT",
  595. target_id=104,
  596. real_name="管理员新名",
  597. organization_name="新组织",
  598. request_id="b7-request-zeta",
  599. ),
  600. audit(
  601. "admin",
  602. datetime(2026, 1, 7, 2, 0),
  603. "VIEW_DOCUMENT",
  604. result="FAILURE",
  605. target_id=105,
  606. real_name="管理员新名",
  607. organization_name="新组织",
  608. request_id="b7-request-eta",
  609. ),
  610. audit(
  611. None,
  612. datetime(2026, 1, 8, 0, 0),
  613. "LOGIN",
  614. result="FAILURE",
  615. target_type="AUTH",
  616. target_id=None,
  617. target_name="unknown",
  618. failure_reason="用户名不存在",
  619. request_id="b7-request-theta",
  620. ),
  621. audit(
  622. "auditor",
  623. datetime(2026, 2, 2, 0, 0),
  624. "LOGOUT",
  625. target_type="AUTH",
  626. target_id=users["auditor"].id,
  627. target_name="auditor",
  628. request_id="b7-request-iota",
  629. ),
  630. ]
  631. db.session.commit()
  632. b4_app.config["B7_AUDIT_IDS"] = [row.id for row in rows]
  633. b4_app.config["B7_USER_IDS"] = {
  634. name: item.id for name, item in users.items()
  635. }
  636. return b4_app
  637. @pytest.fixture()
  638. def b7_client(b7_app):
  639. return b7_app.test_client()