test_document_organization.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. """文档归属部门行为测试(T1.2)。
  2. 覆盖:
  3. - 上传默认取创建者当前部门;显式 organizationId 可指定其他启用部门
  4. - 无效/停用部门 → 404 RESOURCE_NOT_FOUND
  5. - PUT /documents/{id} 修改归属部门
  6. - 编辑缺省 organizationId 沿用现有归属,不被编辑人部门覆盖
  7. - 非管理员原样回传其他部门归属视为未变更;改挂其他部门 → 400 INVALID_ARGUMENT
  8. - 组织改名级联 Document.organization_name 冗余字段
  9. - 用户调岗不联动历史文档归属
  10. - 附件库条目输出 organizationName
  11. """
  12. from __future__ import annotations
  13. import io
  14. import json
  15. import pytest
  16. from docx import Document as WordDocument
  17. from dms.extensions import db
  18. from dms.models import Document, Organization, User
  19. def _auth(token: str) -> dict[str, str]:
  20. return {"Authorization": f"Bearer {token}"}
  21. def _docx_bytes() -> bytes:
  22. stream = io.BytesIO()
  23. document = WordDocument()
  24. document.add_paragraph("DO组织行为验证")
  25. document.save(stream)
  26. return stream.getvalue()
  27. def _upload(client, token: str, path: str, metadata: dict):
  28. name = metadata["documentName"]
  29. return client.post(
  30. f"/api/v1/{path}",
  31. data={
  32. "file": (io.BytesIO(_docx_bytes()), f"{name}.docx"),
  33. "metadata": json.dumps(metadata, ensure_ascii=False),
  34. },
  35. headers=_auth(token),
  36. content_type="multipart/form-data",
  37. )
  38. def _leaf_category_id(app) -> str:
  39. from dms.models import Category
  40. with app.app_context():
  41. category = db.session.scalar(
  42. db.select(Category).where(Category.category_code == "CHILD_A1")
  43. )
  44. assert category is not None
  45. return str(category.id)
  46. def _org_by_code(app, code: str):
  47. with app.app_context():
  48. org = db.session.scalar(
  49. db.select(Organization).where(Organization.org_code == code)
  50. )
  51. assert org is not None
  52. return org.id, org.org_name, org.parent_id, org.row_version
  53. def _main_metadata(leaf_id: str, name: str, **extra) -> dict:
  54. return {
  55. "documentName": name,
  56. "documentType": "MAIN",
  57. "summary": "DO摘要",
  58. "categoryId": leaf_id,
  59. "securityLevel": "INTERNAL",
  60. "tags": ["DO"],
  61. **extra,
  62. }
  63. @pytest.fixture()
  64. def do_app(b3_app):
  65. with b3_app.app_context():
  66. root = db.session.scalar(
  67. db.select(Organization).where(Organization.org_code == "ORG_ROOT")
  68. )
  69. assert root is not None
  70. org_a = Organization(
  71. org_code="ORG_DO_A",
  72. org_name="DO甲部门",
  73. org_path=f"{root.org_path}/DO甲部门",
  74. parent_id=root.id,
  75. sort_no=80,
  76. status="ENABLED",
  77. )
  78. org_disabled = Organization(
  79. org_code="ORG_DO_DISABLED",
  80. org_name="DO停用部门",
  81. org_path=f"{root.org_path}/DO停用部门",
  82. parent_id=root.id,
  83. sort_no=81,
  84. status="DISABLED",
  85. )
  86. db.session.add_all([org_a, org_disabled])
  87. db.session.commit()
  88. b3_app.config["DO_IDS"] = {
  89. "org_a": org_a.id,
  90. "org_disabled": org_disabled.id,
  91. }
  92. return b3_app
  93. @pytest.fixture()
  94. def do_client(do_app):
  95. return do_app.test_client()
  96. def test_upload_defaults_to_creator_organization(do_app, do_client, token_for):
  97. response = _upload(
  98. do_client,
  99. token_for("admin"),
  100. "documents",
  101. _main_metadata(_leaf_category_id(do_app), "DO_TEST_默认部门主案"),
  102. )
  103. assert response.status_code == 201
  104. item = response.get_json()["data"]
  105. with do_app.app_context():
  106. admin = db.session.scalar(db.select(User).where(User.username == "admin"))
  107. assert item["organizationId"] == str(admin.organization_id)
  108. assert item["organizationName"] == admin.organization_name
  109. def test_upload_with_explicit_enabled_organization(do_app, do_client, token_for):
  110. org_a_id = do_app.config["DO_IDS"]["org_a"]
  111. response = _upload(
  112. do_client,
  113. token_for("admin"),
  114. "documents",
  115. _main_metadata(
  116. _leaf_category_id(do_app),
  117. "DO_TEST_指定部门主案",
  118. organizationId=str(org_a_id),
  119. ),
  120. )
  121. assert response.status_code == 201
  122. item = response.get_json()["data"]
  123. assert item["organizationId"] == str(org_a_id)
  124. assert item["organizationName"] == "DO甲部门"
  125. @pytest.mark.parametrize(
  126. "org_code",
  127. ["missing", "ORG_DO_DISABLED"],
  128. )
  129. def test_upload_rejects_invalid_or_disabled_organization(
  130. do_app, do_client, token_for, org_code
  131. ):
  132. raw_id = (
  133. "99999999"
  134. if org_code == "missing"
  135. else str(do_app.config["DO_IDS"][org_code])
  136. )
  137. response = _upload(
  138. do_client,
  139. token_for("admin"),
  140. "documents",
  141. _main_metadata(
  142. _leaf_category_id(do_app),
  143. f"DO_TEST_非法部门主案_{org_code}",
  144. organizationId=raw_id,
  145. ),
  146. )
  147. assert response.status_code == 404
  148. assert response.get_json()["code"] == "RESOURCE_NOT_FOUND"
  149. def test_update_document_organization(do_app, do_client, token_for):
  150. token = token_for("admin")
  151. org_a_id = do_app.config["DO_IDS"]["org_a"]
  152. created = _upload(
  153. do_client,
  154. token,
  155. "documents",
  156. _main_metadata(_leaf_category_id(do_app), "DO_TEST_修改部门主案"),
  157. ).get_json()["data"]
  158. assert created["organizationName"] != "DO甲部门"
  159. updated = do_client.put(
  160. f"/api/v1/documents/{created['id']}",
  161. json={
  162. "documentName": created["documentName"],
  163. "summary": created["summary"],
  164. "categoryId": created["categoryId"],
  165. "securityLevel": created["securityLevel"],
  166. "tags": created["tags"],
  167. "rowVersion": created["rowVersion"],
  168. "organizationId": str(org_a_id),
  169. },
  170. headers=_auth(token),
  171. )
  172. assert updated.status_code == 200
  173. assert updated.get_json()["data"]["organizationName"] == "DO甲部门"
  174. listed = do_client.get(
  175. "/api/v1/documents",
  176. query_string={
  177. "keyword": "DO_TEST_修改部门主案",
  178. "organizationKeyword": "DO甲部",
  179. },
  180. headers=_auth(token),
  181. ).get_json()["data"]
  182. assert [item["documentName"] for item in listed["items"]] == [
  183. "DO_TEST_修改部门主案"
  184. ]
  185. def _edit_payload(item: dict, **extra) -> dict:
  186. return {
  187. "documentName": item["documentName"],
  188. "summary": item["summary"],
  189. "categoryId": item["categoryId"],
  190. "securityLevel": item["securityLevel"],
  191. "tags": item["tags"],
  192. "rowVersion": item["rowVersion"],
  193. **extra,
  194. }
  195. def test_update_without_organization_keeps_current(
  196. do_app, do_client, token_for
  197. ):
  198. """编辑未携带 organizationId 时沿用文档现有归属,不被编辑人部门覆盖。"""
  199. token = token_for("admin")
  200. org_a_id = do_app.config["DO_IDS"]["org_a"]
  201. created = _upload(
  202. do_client,
  203. token,
  204. "documents",
  205. _main_metadata(
  206. _leaf_category_id(do_app),
  207. "DO_TEST_编辑不改部门主案",
  208. organizationId=str(org_a_id),
  209. ),
  210. ).get_json()["data"]
  211. updated = do_client.put(
  212. f"/api/v1/documents/{created['id']}",
  213. json=_edit_payload(created),
  214. headers=_auth(token),
  215. )
  216. assert updated.status_code == 200
  217. data = updated.get_json()["data"]
  218. assert data["organizationId"] == str(org_a_id)
  219. assert data["organizationName"] == "DO甲部门"
  220. def test_non_admin_can_resubmit_other_organization_but_not_reassign(
  221. do_app, do_client, token_for
  222. ):
  223. """非管理员原样回传其他部门归属视为未变更;改挂其他部门被拒绝。"""
  224. admin_token = token_for("admin")
  225. user_token = token_for("user")
  226. org_a_id = do_app.config["DO_IDS"]["org_a"]
  227. comms_id, _, _, _ = _org_by_code(do_app, "ORG_COMMS")
  228. created = _upload(
  229. do_client,
  230. admin_token,
  231. "documents",
  232. _main_metadata(
  233. _leaf_category_id(do_app),
  234. "DO_TEST_普通用户编辑他部门主案",
  235. organizationId=str(org_a_id),
  236. ),
  237. ).get_json()["data"]
  238. with do_app.app_context():
  239. user_id = db.session.scalar(
  240. db.select(User.id).where(User.username == "user")
  241. )
  242. # 授予普通用户该主案EDIT权限(CUSTOM可见范围+用户授权)
  243. granted = do_client.put(
  244. f"/api/v1/documents/{created['id']}/permissions",
  245. json={
  246. "visibilityType": "CUSTOM",
  247. "documentRowVersion": created["rowVersion"],
  248. "entries": [
  249. {
  250. "subjectType": "USER",
  251. "subjectId": str(user_id),
  252. "canView": True,
  253. "canDownload": True,
  254. "canEdit": True,
  255. "canManagePermission": False,
  256. "canDelete": False,
  257. }
  258. ],
  259. },
  260. headers=_auth(admin_token),
  261. )
  262. assert granted.status_code == 200, granted.get_json()
  263. # 保存权限会推进rowVersion,重新读取详情
  264. latest = do_client.get(
  265. f"/api/v1/documents/{created['id']}", headers=_auth(user_token)
  266. ).get_json()["data"]
  267. assert "EDIT" in latest["allowedActions"]
  268. # 原样回传其他部门(DO甲部门)归属:视为未变更,允许保存
  269. unchanged = do_client.put(
  270. f"/api/v1/documents/{created['id']}",
  271. json=_edit_payload(latest, organizationId=str(org_a_id)),
  272. headers=_auth(user_token),
  273. )
  274. assert unchanged.status_code == 200, unchanged.get_json()
  275. assert unchanged.get_json()["data"]["organizationId"] == str(org_a_id)
  276. # 改挂到通信部:非管理员被拒绝
  277. reassigned = do_client.put(
  278. f"/api/v1/documents/{created['id']}",
  279. json=_edit_payload(
  280. unchanged.get_json()["data"], organizationId=str(comms_id)
  281. ),
  282. headers=_auth(user_token),
  283. )
  284. assert reassigned.status_code == 400
  285. body = reassigned.get_json()
  286. assert body["code"] == "INVALID_ARGUMENT"
  287. assert body["message"] == "仅管理员可将文档调整到其他部门"
  288. def test_organization_rename_cascades_document_name(do_app, do_client, token_for):
  289. token = token_for("admin")
  290. org_a_id, _, parent_id, row_version = _org_by_code(do_app, "ORG_DO_A")
  291. created = _upload(
  292. do_client,
  293. token,
  294. "documents",
  295. _main_metadata(
  296. _leaf_category_id(do_app),
  297. "DO_TEST_改名级联主案",
  298. organizationId=str(org_a_id),
  299. ),
  300. ).get_json()["data"]
  301. assert created["organizationName"] == "DO甲部门"
  302. def rename(name: str, version: int) -> int:
  303. response = do_client.put(
  304. f"/api/v1/organizations/{org_a_id}",
  305. json={
  306. "orgName": name,
  307. "parentId": str(parent_id),
  308. "sortNo": 80,
  309. "status": "ENABLED",
  310. "rowVersion": version,
  311. },
  312. headers=_auth(token),
  313. )
  314. assert response.status_code == 200, response.get_json()
  315. return response.get_json()["data"]["rowVersion"]
  316. try:
  317. row_version = rename("DO甲部门新名", row_version)
  318. listed = do_client.get(
  319. "/api/v1/documents",
  320. query_string={"keyword": "DO_TEST_改名级联主案"},
  321. headers=_auth(token),
  322. ).get_json()["data"]
  323. assert listed["items"][0]["organizationName"] == "DO甲部门新名"
  324. finally:
  325. rename("DO甲部门", row_version)
  326. def test_user_transfer_does_not_cascade_documents(do_app, do_client, token_for):
  327. token = token_for("admin")
  328. org_a_id = do_app.config["DO_IDS"]["org_a"]
  329. comms_id, comms_name, _, _ = _org_by_code(do_app, "ORG_COMMS")
  330. created_user = do_client.post(
  331. "/api/v1/users",
  332. json={
  333. "username": "do_transfer_user",
  334. "realName": "调岗验证用户",
  335. "initialPassword": "Initial@2026",
  336. "organizationId": str(org_a_id),
  337. "roleCode": "USER",
  338. "securityLevel": "INTERNAL",
  339. "status": "ENABLED",
  340. },
  341. headers=_auth(token),
  342. )
  343. assert created_user.status_code == 201, created_user.get_json()
  344. user_id = created_user.get_json()["data"]["id"]
  345. with do_app.app_context():
  346. leaf = db.session.scalar(
  347. db.select(Document).where(
  348. Document.document_name == "DO_TEST_修改部门主案",
  349. Document.is_deleted.is_(False),
  350. )
  351. )
  352. assert leaf is not None
  353. document = Document(
  354. document_name="DO_TEST_调岗不联动主案",
  355. summary="调岗不联动",
  356. document_type="MAIN",
  357. document_status="PUBLISHED",
  358. security_level="INTERNAL",
  359. visibility_type="CUSTOM",
  360. organization_id=org_a_id,
  361. organization_name="DO甲部门",
  362. tags=["DO"],
  363. original_file_name="DO_TEST_调岗不联动主案.docx",
  364. file_relative_path="original/DO_TEST_调岗不联动主案.docx",
  365. file_extension="docx",
  366. mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  367. file_size=64,
  368. file_hash="d" * 64,
  369. search_text="DO_TEST_调岗不联动主案",
  370. created_by=int(user_id),
  371. updated_by=int(user_id),
  372. created_by_name="调岗验证用户",
  373. updated_by_name="调岗验证用户",
  374. category_id=leaf.category_id,
  375. )
  376. db.session.add(document)
  377. user = db.session.get(User, int(user_id))
  378. row_version = user.row_version
  379. db.session.commit()
  380. transferred = do_client.put(
  381. f"/api/v1/users/{user_id}",
  382. json={
  383. "realName": "调岗验证用户",
  384. "organizationId": str(comms_id),
  385. "roleCode": "USER",
  386. "securityLevel": "INTERNAL",
  387. "status": "ENABLED",
  388. "rowVersion": row_version,
  389. },
  390. headers=_auth(token),
  391. )
  392. assert transferred.status_code == 200, transferred.get_json()
  393. assert transferred.get_json()["data"]["organizationName"] == comms_name
  394. with do_app.app_context():
  395. current = db.session.scalar(
  396. db.select(Document).where(
  397. Document.document_name == "DO_TEST_调岗不联动主案",
  398. Document.is_deleted.is_(False),
  399. )
  400. )
  401. assert current.organization_id == org_a_id
  402. assert current.organization_name == "DO甲部门"
  403. def test_attachment_library_exposes_organization_name(
  404. do_app, do_client, token_for
  405. ):
  406. token = token_for("admin")
  407. created = _upload(
  408. do_client,
  409. token,
  410. "attachments",
  411. {
  412. "documentName": "DO_TEST_部门附件",
  413. "attachmentType": "OTHER",
  414. "summary": "DO附件",
  415. "tags": ["DO"],
  416. },
  417. )
  418. assert created.status_code == 201
  419. listed = do_client.get(
  420. "/api/v1/attachments",
  421. query_string={"keyword": "DO_TEST_部门附件"},
  422. headers=_auth(token),
  423. ).get_json()["data"]
  424. assert len(listed["items"]) == 1
  425. with do_app.app_context():
  426. admin = db.session.scalar(db.select(User).where(User.username == "admin"))
  427. assert listed["items"][0]["organizationId"] == str(admin.organization_id)
  428. assert listed["items"][0]["organizationName"] == admin.organization_name