conftest.py 21 KB

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