conftest.py 20 KB

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