test_b5_cors_headers.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from __future__ import annotations
  2. import io
  3. import json
  4. from urllib.parse import quote
  5. from dms.api.v1.blueprint import _merge_exposed_headers
  6. ORIGIN = "http://127.0.0.1:9346"
  7. EXPOSED = {"content-disposition", "x-request-id"}
  8. def _headers(token: str | None = None) -> dict[str, str]:
  9. headers = {"Origin": ORIGIN}
  10. if token:
  11. headers["Authorization"] = f"Bearer {token}"
  12. return headers
  13. def _exposed(response) -> list[str]:
  14. return [
  15. value.strip().lower()
  16. for value in response.headers["Access-Control-Expose-Headers"].split(",")
  17. if value.strip()
  18. ]
  19. def test_dms_json_success_exposes_headers_and_request_id_matches(client):
  20. response = client.get("/api/v1/health", headers=_headers())
  21. payload = response.get_json()
  22. # 白名单化后默认不启用跨域:未配置 DMS_CORS_ORIGINS 时不回显 Allow-Origin
  23. assert "Access-Control-Allow-Origin" not in response.headers
  24. assert EXPOSED <= set(_exposed(response))
  25. assert "*" not in _exposed(response)
  26. assert response.headers["X-Request-Id"] == payload["requestId"]
  27. def test_dms_401_and_routing_404_expose_request_id(client):
  28. unauthorized = client.get("/api/v1/documents", headers=_headers())
  29. assert unauthorized.status_code == 401
  30. assert EXPOSED <= set(_exposed(unauthorized))
  31. assert (
  32. unauthorized.headers["X-Request-Id"]
  33. == unauthorized.get_json()["requestId"]
  34. )
  35. missing = client.get("/api/v1/not-implemented", headers=_headers())
  36. assert missing.status_code == 404
  37. assert EXPOSED <= set(_exposed(missing))
  38. assert missing.headers["X-Request-Id"] == missing.get_json()["requestId"]
  39. def test_expose_header_merge_is_case_insensitive_and_deduplicated():
  40. value = _merge_exposed_headers(
  41. "ETag, x-request-id, etag, CONTENT-DISPOSITION"
  42. )
  43. parts = [item.strip().lower() for item in value.split(",")]
  44. assert parts == ["etag", "x-request-id", "content-disposition"]
  45. assert len(parts) == len(set(parts))
  46. assert "*" not in parts
  47. def test_preview_and_chinese_download_expose_headers_once(
  48. b5_app, b5_client, token_for
  49. ):
  50. from dms.extensions import db
  51. from dms.models import AuditLog, Document
  52. token = token_for("admin")
  53. filename = "中文下载文件.pdf"
  54. created = b5_client.post(
  55. "/api/v1/attachments",
  56. data={
  57. "file": (io.BytesIO(b"%PDF-1.4\nCORS"), filename),
  58. "metadata": json.dumps(
  59. {
  60. "documentName": "B5_C1_TEST_中文下载",
  61. "attachmentType": "SUPPORT_ACHIEVEMENT",
  62. "summary": "CORS响应头验证",
  63. "tags": ["CORS"],
  64. },
  65. ensure_ascii=False,
  66. ),
  67. },
  68. headers=_headers(token),
  69. content_type="multipart/form-data",
  70. ).get_json()["data"]
  71. preview = b5_client.get(
  72. f"/api/v1/documents/{created['id']}/preview",
  73. headers=_headers(token),
  74. )
  75. assert preview.status_code == 200
  76. assert EXPOSED <= set(_exposed(preview))
  77. assert preview.headers["Content-Disposition"].startswith("inline")
  78. preview.close()
  79. download = b5_client.get(
  80. f"/api/v1/documents/{created['id']}/download",
  81. headers=_headers(token),
  82. )
  83. disposition = download.headers["Content-Disposition"]
  84. assert download.status_code == 200
  85. assert download.headers["Access-Control-Allow-Origin"] == ORIGIN
  86. assert EXPOSED <= set(_exposed(download))
  87. assert "filename=" in disposition
  88. assert "filename*=UTF-8''" in disposition
  89. assert quote(filename, safe="") in disposition
  90. assert download.headers["X-Request-Id"]
  91. download.close()
  92. with b5_app.app_context():
  93. document = db.session.get(Document, int(created["id"]))
  94. audit_count = db.session.scalar(
  95. db.select(db.func.count(AuditLog.id)).where(
  96. AuditLog.target_id == document.id,
  97. AuditLog.action_type == "DOWNLOAD_DOCUMENT",
  98. )
  99. )
  100. assert document.download_count == 1
  101. assert audit_count == 1
  102. def test_ai_response_does_not_gain_dms_expose_headers(client):
  103. """非 DMS 蓝图路由(/api/health)不带 DMS Expose-Headers;默认亦不开放跨域。"""
  104. response = client.get("/api/health", headers=_headers())
  105. assert response.status_code == 200
  106. assert "Access-Control-Allow-Origin" not in response.headers
  107. assert "Access-Control-Expose-Headers" not in response.headers