test_document_organization.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. """文档归属部门行为测试(T1.2)。
  2. 覆盖:
  3. - 上传默认取创建者当前部门;显式 organizationId 可指定其他启用部门
  4. - 无效/停用部门 → 404 RESOURCE_NOT_FOUND
  5. - PUT /documents/{id} 修改归属部门
  6. - 组织改名级联 Document.organization_name 冗余字段
  7. - 用户调岗不联动历史文档归属
  8. - 附件库条目输出 organizationName
  9. """
  10. from __future__ import annotations
  11. import io
  12. import json
  13. import pytest
  14. from docx import Document as WordDocument
  15. from dms.extensions import db
  16. from dms.models import Document, Organization, User
  17. def _auth(token: str) -> dict[str, str]:
  18. return {"Authorization": f"Bearer {token}"}
  19. def _docx_bytes() -> bytes:
  20. stream = io.BytesIO()
  21. document = WordDocument()
  22. document.add_paragraph("DO组织行为验证")
  23. document.save(stream)
  24. return stream.getvalue()
  25. def _upload(client, token: str, path: str, metadata: dict):
  26. name = metadata["documentName"]
  27. return client.post(
  28. f"/api/v1/{path}",
  29. data={
  30. "file": (io.BytesIO(_docx_bytes()), f"{name}.docx"),
  31. "metadata": json.dumps(metadata, ensure_ascii=False),
  32. },
  33. headers=_auth(token),
  34. content_type="multipart/form-data",
  35. )
  36. def _leaf_category_id(app) -> str:
  37. from dms.models import Category
  38. with app.app_context():
  39. category = db.session.scalar(
  40. db.select(Category).where(Category.category_code == "CHILD_A1")
  41. )
  42. assert category is not None
  43. return str(category.id)
  44. def _org_by_code(app, code: str):
  45. with app.app_context():
  46. org = db.session.scalar(
  47. db.select(Organization).where(Organization.org_code == code)
  48. )
  49. assert org is not None
  50. return org.id, org.org_name, org.parent_id, org.row_version
  51. def _main_metadata(leaf_id: str, name: str, **extra) -> dict:
  52. return {
  53. "documentName": name,
  54. "documentType": "MAIN",
  55. "summary": "DO摘要",
  56. "categoryId": leaf_id,
  57. "securityLevel": "INTERNAL",
  58. "tags": ["DO"],
  59. **extra,
  60. }
  61. @pytest.fixture()
  62. def do_app(b3_app):
  63. with b3_app.app_context():
  64. root = db.session.scalar(
  65. db.select(Organization).where(Organization.org_code == "ORG_ROOT")
  66. )
  67. assert root is not None
  68. org_a = Organization(
  69. org_code="ORG_DO_A",
  70. org_name="DO甲部门",
  71. org_path=f"{root.org_path}/DO甲部门",
  72. parent_id=root.id,
  73. sort_no=80,
  74. status="ENABLED",
  75. )
  76. org_disabled = Organization(
  77. org_code="ORG_DO_DISABLED",
  78. org_name="DO停用部门",
  79. org_path=f"{root.org_path}/DO停用部门",
  80. parent_id=root.id,
  81. sort_no=81,
  82. status="DISABLED",
  83. )
  84. db.session.add_all([org_a, org_disabled])
  85. db.session.commit()
  86. b3_app.config["DO_IDS"] = {
  87. "org_a": org_a.id,
  88. "org_disabled": org_disabled.id,
  89. }
  90. return b3_app
  91. @pytest.fixture()
  92. def do_client(do_app):
  93. return do_app.test_client()
  94. def test_upload_defaults_to_creator_organization(do_app, do_client, token_for):
  95. response = _upload(
  96. do_client,
  97. token_for("admin"),
  98. "documents",
  99. _main_metadata(_leaf_category_id(do_app), "DO_TEST_默认部门主案"),
  100. )
  101. assert response.status_code == 201
  102. item = response.get_json()["data"]
  103. with do_app.app_context():
  104. admin = db.session.scalar(db.select(User).where(User.username == "admin"))
  105. assert item["organizationId"] == str(admin.organization_id)
  106. assert item["organizationName"] == admin.organization_name
  107. def test_upload_with_explicit_enabled_organization(do_app, do_client, token_for):
  108. org_a_id = do_app.config["DO_IDS"]["org_a"]
  109. response = _upload(
  110. do_client,
  111. token_for("admin"),
  112. "documents",
  113. _main_metadata(
  114. _leaf_category_id(do_app),
  115. "DO_TEST_指定部门主案",
  116. organizationId=str(org_a_id),
  117. ),
  118. )
  119. assert response.status_code == 201
  120. item = response.get_json()["data"]
  121. assert item["organizationId"] == str(org_a_id)
  122. assert item["organizationName"] == "DO甲部门"
  123. @pytest.mark.parametrize(
  124. "org_code",
  125. ["missing", "ORG_DO_DISABLED"],
  126. )
  127. def test_upload_rejects_invalid_or_disabled_organization(
  128. do_app, do_client, token_for, org_code
  129. ):
  130. raw_id = (
  131. "99999999"
  132. if org_code == "missing"
  133. else str(do_app.config["DO_IDS"][org_code])
  134. )
  135. response = _upload(
  136. do_client,
  137. token_for("admin"),
  138. "documents",
  139. _main_metadata(
  140. _leaf_category_id(do_app),
  141. f"DO_TEST_非法部门主案_{org_code}",
  142. organizationId=raw_id,
  143. ),
  144. )
  145. assert response.status_code == 404
  146. assert response.get_json()["code"] == "RESOURCE_NOT_FOUND"
  147. def test_update_document_organization(do_app, do_client, token_for):
  148. token = token_for("admin")
  149. org_a_id = do_app.config["DO_IDS"]["org_a"]
  150. created = _upload(
  151. do_client,
  152. token,
  153. "documents",
  154. _main_metadata(_leaf_category_id(do_app), "DO_TEST_修改部门主案"),
  155. ).get_json()["data"]
  156. assert created["organizationName"] != "DO甲部门"
  157. updated = do_client.put(
  158. f"/api/v1/documents/{created['id']}",
  159. json={
  160. "documentName": created["documentName"],
  161. "summary": created["summary"],
  162. "categoryId": created["categoryId"],
  163. "securityLevel": created["securityLevel"],
  164. "tags": created["tags"],
  165. "rowVersion": created["rowVersion"],
  166. "organizationId": str(org_a_id),
  167. },
  168. headers=_auth(token),
  169. )
  170. assert updated.status_code == 200
  171. assert updated.get_json()["data"]["organizationName"] == "DO甲部门"
  172. listed = do_client.get(
  173. "/api/v1/documents",
  174. query_string={
  175. "keyword": "DO_TEST_修改部门主案",
  176. "organizationKeyword": "DO甲部",
  177. },
  178. headers=_auth(token),
  179. ).get_json()["data"]
  180. assert [item["documentName"] for item in listed["items"]] == [
  181. "DO_TEST_修改部门主案"
  182. ]
  183. def test_organization_rename_cascades_document_name(do_app, do_client, token_for):
  184. token = token_for("admin")
  185. org_a_id, _, parent_id, row_version = _org_by_code(do_app, "ORG_DO_A")
  186. created = _upload(
  187. do_client,
  188. token,
  189. "documents",
  190. _main_metadata(
  191. _leaf_category_id(do_app),
  192. "DO_TEST_改名级联主案",
  193. organizationId=str(org_a_id),
  194. ),
  195. ).get_json()["data"]
  196. assert created["organizationName"] == "DO甲部门"
  197. def rename(name: str, version: int) -> int:
  198. response = do_client.put(
  199. f"/api/v1/organizations/{org_a_id}",
  200. json={
  201. "orgName": name,
  202. "parentId": str(parent_id),
  203. "sortNo": 80,
  204. "status": "ENABLED",
  205. "rowVersion": version,
  206. },
  207. headers=_auth(token),
  208. )
  209. assert response.status_code == 200, response.get_json()
  210. return response.get_json()["data"]["rowVersion"]
  211. try:
  212. row_version = rename("DO甲部门新名", row_version)
  213. listed = do_client.get(
  214. "/api/v1/documents",
  215. query_string={"keyword": "DO_TEST_改名级联主案"},
  216. headers=_auth(token),
  217. ).get_json()["data"]
  218. assert listed["items"][0]["organizationName"] == "DO甲部门新名"
  219. finally:
  220. rename("DO甲部门", row_version)
  221. def test_user_transfer_does_not_cascade_documents(do_app, do_client, token_for):
  222. token = token_for("admin")
  223. org_a_id = do_app.config["DO_IDS"]["org_a"]
  224. comms_id, comms_name, _, _ = _org_by_code(do_app, "ORG_COMMS")
  225. created_user = do_client.post(
  226. "/api/v1/users",
  227. json={
  228. "username": "do_transfer_user",
  229. "realName": "调岗验证用户",
  230. "initialPassword": "Initial@2026",
  231. "organizationId": str(org_a_id),
  232. "roleCode": "USER",
  233. "securityLevel": "INTERNAL",
  234. "status": "ENABLED",
  235. },
  236. headers=_auth(token),
  237. )
  238. assert created_user.status_code == 201, created_user.get_json()
  239. user_id = created_user.get_json()["data"]["id"]
  240. with do_app.app_context():
  241. leaf = db.session.scalar(
  242. db.select(Document).where(
  243. Document.document_name == "DO_TEST_修改部门主案",
  244. Document.is_deleted.is_(False),
  245. )
  246. )
  247. assert leaf is not None
  248. document = Document(
  249. document_name="DO_TEST_调岗不联动主案",
  250. summary="调岗不联动",
  251. document_type="MAIN",
  252. document_status="PUBLISHED",
  253. security_level="INTERNAL",
  254. visibility_type="CUSTOM",
  255. organization_id=org_a_id,
  256. organization_name="DO甲部门",
  257. tags=["DO"],
  258. original_file_name="DO_TEST_调岗不联动主案.docx",
  259. file_relative_path="original/DO_TEST_调岗不联动主案.docx",
  260. file_extension="docx",
  261. mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  262. file_size=64,
  263. file_hash="d" * 64,
  264. search_text="DO_TEST_调岗不联动主案",
  265. created_by=int(user_id),
  266. updated_by=int(user_id),
  267. created_by_name="调岗验证用户",
  268. updated_by_name="调岗验证用户",
  269. category_id=leaf.category_id,
  270. )
  271. db.session.add(document)
  272. user = db.session.get(User, int(user_id))
  273. row_version = user.row_version
  274. db.session.commit()
  275. transferred = do_client.put(
  276. f"/api/v1/users/{user_id}",
  277. json={
  278. "realName": "调岗验证用户",
  279. "organizationId": str(comms_id),
  280. "roleCode": "USER",
  281. "securityLevel": "INTERNAL",
  282. "status": "ENABLED",
  283. "rowVersion": row_version,
  284. },
  285. headers=_auth(token),
  286. )
  287. assert transferred.status_code == 200, transferred.get_json()
  288. assert transferred.get_json()["data"]["organizationName"] == comms_name
  289. with do_app.app_context():
  290. current = db.session.scalar(
  291. db.select(Document).where(
  292. Document.document_name == "DO_TEST_调岗不联动主案",
  293. Document.is_deleted.is_(False),
  294. )
  295. )
  296. assert current.organization_id == org_a_id
  297. assert current.organization_name == "DO甲部门"
  298. def test_attachment_library_exposes_organization_name(
  299. do_app, do_client, token_for
  300. ):
  301. token = token_for("admin")
  302. created = _upload(
  303. do_client,
  304. token,
  305. "attachments",
  306. {
  307. "documentName": "DO_TEST_部门附件",
  308. "attachmentType": "OTHER",
  309. "summary": "DO附件",
  310. "tags": ["DO"],
  311. },
  312. )
  313. assert created.status_code == 201
  314. listed = do_client.get(
  315. "/api/v1/attachments",
  316. query_string={"keyword": "DO_TEST_部门附件"},
  317. headers=_auth(token),
  318. ).get_json()["data"]
  319. assert len(listed["items"]) == 1
  320. with do_app.app_context():
  321. admin = db.session.scalar(db.select(User).where(User.username == "admin"))
  322. assert listed["items"][0]["organizationId"] == str(admin.organization_id)
  323. assert listed["items"][0]["organizationName"] == admin.organization_name