conftest.py 21 KB

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