test_b6_vouchers.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """B6借阅授权审批单(PDF凭证)上传/下载/清除的接口契约测试(契约1.19 12.3-12.5)。"""
  2. from __future__ import annotations
  3. import io
  4. VOUCHER_PDF = b"%PDF-1.4\nB6 voucher original\n%%EOF"
  5. REPLACED_PDF = b"%PDF-1.4\nB6 voucher replaced\n%%EOF"
  6. def _headers(token: str) -> dict[str, str]:
  7. return {"Authorization": f"Bearer {token}"}
  8. def _doc_id(app, name: str) -> str:
  9. from dms.extensions import db
  10. from dms.models import Document
  11. with app.app_context():
  12. document_id = db.session.scalar(
  13. db.select(Document.id).where(Document.document_name == name)
  14. )
  15. assert document_id is not None
  16. return str(document_id)
  17. def _entry_id(b6_client, login_token, document_id) -> str:
  18. response = b6_client.get(
  19. f"/api/v1/documents/{document_id}/permissions",
  20. headers=_headers(login_token),
  21. )
  22. assert response.status_code == 200
  23. entries = response.get_json()["data"]["entries"]
  24. assert entries
  25. return entries[0]["id"]
  26. def _upload(b6_client, login_token, document_id, entry_id, content, name):
  27. return b6_client.post(
  28. f"/api/v1/documents/{document_id}/permissions/entries/{entry_id}"
  29. "/voucher",
  30. data={"file": (io.BytesIO(content), name)},
  31. headers=_headers(login_token),
  32. content_type="multipart/form-data",
  33. )
  34. def test_voucher_upload_download_replace_delete_and_audit_flow(
  35. b6_app, b6_client, login_token
  36. ):
  37. from dms.extensions import db
  38. from dms.models import AuditLog
  39. main_id = _doc_id(b6_app, "B4_TEST_自定义主案")
  40. entry_id = _entry_id(b6_client, login_token, main_id)
  41. # 未上传凭证:下载404,详情凭证字段为空
  42. missing = b6_client.get(
  43. f"/api/v1/documents/{main_id}/permissions/entries/{entry_id}/voucher",
  44. headers=_headers(login_token),
  45. )
  46. assert missing.status_code == 404
  47. assert missing.get_json()["code"] == "RESOURCE_NOT_FOUND"
  48. uploaded = _upload(
  49. b6_client, login_token, main_id, entry_id, VOUCHER_PDF, "借阅审批单.pdf"
  50. )
  51. assert uploaded.status_code == 200, uploaded.get_json()
  52. data = uploaded.get_json()["data"]
  53. assert data["documentId"] == main_id
  54. assert data["entryId"] == entry_id
  55. assert data["voucherFileName"] == "借阅审批单.pdf"
  56. assert data["voucherFileSize"] == len(VOUCHER_PDF)
  57. assert data["voucherUploadedAt"]
  58. detail = b6_client.get(
  59. f"/api/v1/documents/{main_id}/permissions",
  60. headers=_headers(login_token),
  61. ).get_json()["data"]["entries"]
  62. assert [item["id"] for item in detail] == [entry_id]
  63. assert detail[0]["voucherFileName"] == "借阅审批单.pdf"
  64. assert detail[0]["voucherFileSize"] == len(VOUCHER_PDF)
  65. assert detail[0]["voucherUploadedAt"] == data["voucherUploadedAt"]
  66. downloaded = b6_client.get(
  67. f"/api/v1/documents/{main_id}/permissions/entries/{entry_id}/voucher",
  68. headers=_headers(login_token),
  69. )
  70. assert downloaded.status_code == 200
  71. assert downloaded.data == VOUCHER_PDF
  72. assert downloaded.mimetype == "application/pdf"
  73. assert "attachment" in downloaded.headers["Content-Disposition"]
  74. # 重复上传为替换语义:凭证字段覆盖,下载返回新内容
  75. replaced = _upload(
  76. b6_client, login_token, main_id, entry_id, REPLACED_PDF, "替换审批单.pdf"
  77. )
  78. assert replaced.status_code == 200, replaced.get_json()
  79. assert replaced.get_json()["data"]["voucherFileName"] == "替换审批单.pdf"
  80. again = b6_client.get(
  81. f"/api/v1/documents/{main_id}/permissions/entries/{entry_id}/voucher",
  82. headers=_headers(login_token),
  83. )
  84. assert again.data == REPLACED_PDF
  85. removed = b6_client.delete(
  86. f"/api/v1/documents/{main_id}/permissions/entries/{entry_id}/voucher",
  87. headers=_headers(login_token),
  88. )
  89. assert removed.status_code == 200
  90. assert removed.get_json()["data"] == {
  91. "documentId": main_id,
  92. "entryId": entry_id,
  93. }
  94. cleared = b6_client.get(
  95. f"/api/v1/documents/{main_id}/permissions/entries/{entry_id}/voucher",
  96. headers=_headers(login_token),
  97. )
  98. assert cleared.status_code == 404
  99. entries = b6_client.get(
  100. f"/api/v1/documents/{main_id}/permissions",
  101. headers=_headers(login_token),
  102. ).get_json()["data"]["entries"]
  103. assert entries[0]["voucherFileName"] is None
  104. assert entries[0]["voucherFileSize"] is None
  105. assert entries[0]["voucherUploadedAt"] is None
  106. with b6_app.app_context():
  107. actions = db.session.scalars(
  108. db.select(AuditLog.action_type)
  109. .where(AuditLog.target_id == int(main_id))
  110. .order_by(AuditLog.id.asc())
  111. ).all()
  112. assert "UPLOAD_PERMISSION_VOUCHER" in actions
  113. assert "DELETE_PERMISSION_VOUCHER" in actions
  114. def test_voucher_upload_rejects_non_pdf(b6_app, b6_client, login_token):
  115. main_id = _doc_id(b6_app, "B4_TEST_自定义主案")
  116. entry_id = _entry_id(b6_client, login_token, main_id)
  117. wrong_type = _upload(
  118. b6_client, login_token, main_id, entry_id, b"plain text", "凭证.txt"
  119. )
  120. assert wrong_type.status_code == 415
  121. assert wrong_type.get_json()["code"] == "UNSUPPORTED_FILE_TYPE"
  122. no_extension = _upload(
  123. b6_client, login_token, main_id, entry_id, VOUCHER_PDF, "凭证"
  124. )
  125. assert no_extension.status_code == 415
  126. assert no_extension.get_json()["code"] == "UNSUPPORTED_FILE_TYPE"
  127. def test_voucher_requires_config_permission(
  128. b6_app, b6_client, login_token, token_for
  129. ):
  130. main_id = _doc_id(b6_app, "B4_TEST_自定义主案")
  131. entry_id = _entry_id(b6_client, login_token, main_id)
  132. user_headers = _headers(token_for("user"))
  133. base = f"/api/v1/documents/{main_id}/permissions/entries/{entry_id}/voucher"
  134. assert (
  135. b6_client.post(
  136. base,
  137. data={"file": (io.BytesIO(VOUCHER_PDF), "审批单.pdf")},
  138. headers=user_headers,
  139. content_type="multipart/form-data",
  140. ).status_code
  141. == 403
  142. )
  143. assert b6_client.get(base, headers=user_headers).status_code == 403
  144. assert b6_client.delete(base, headers=user_headers).status_code == 403
  145. def test_voucher_unknown_entry_and_inherited_sources(
  146. b6_app, b6_client, login_token
  147. ):
  148. main_id = _doc_id(b6_app, "B4_TEST_自定义主案")
  149. sub_id = _doc_id(b6_app, "B4_TEST_动态继承子案")
  150. attachment_id = _doc_id(b6_app, "B4_TEST_规范附件")
  151. unknown = b6_client.post(
  152. f"/api/v1/documents/{main_id}/permissions/entries/99999999/voucher",
  153. data={"file": (io.BytesIO(VOUCHER_PDF), "审批单.pdf")},
  154. headers=_headers(login_token),
  155. content_type="multipart/form-data",
  156. )
  157. assert unknown.status_code == 404
  158. assert unknown.get_json()["code"] == "RESOURCE_NOT_FOUND"
  159. sub = b6_client.get(
  160. f"/api/v1/documents/{sub_id}/permissions/entries/1/voucher",
  161. headers=_headers(login_token),
  162. )
  163. assert sub.status_code == 409
  164. assert sub.get_json()["code"] == "SUB_PLAN_PERMISSION_INHERITED"
  165. attachment = b6_client.get(
  166. f"/api/v1/documents/{attachment_id}/permissions/entries/1/voucher",
  167. headers=_headers(login_token),
  168. )
  169. assert attachment.status_code == 400
  170. assert attachment.get_json()["code"] == "ATTACHMENT_HAS_NO_ACL"