conftest.py 21 KB

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