conftest.py 20 KB

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