| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- from __future__ import annotations
- import io
- import json
- from urllib.parse import quote
- from dms.api.v1.blueprint import _merge_exposed_headers
- ORIGIN = "http://127.0.0.1:9346"
- EXPOSED = {"content-disposition", "x-request-id"}
- def _headers(token: str | None = None) -> dict[str, str]:
- headers = {"Origin": ORIGIN}
- if token:
- headers["Authorization"] = f"Bearer {token}"
- return headers
- def _exposed(response) -> list[str]:
- return [
- value.strip().lower()
- for value in response.headers["Access-Control-Expose-Headers"].split(",")
- if value.strip()
- ]
- def test_dms_json_success_exposes_headers_and_request_id_matches(client):
- response = client.get("/api/v1/health", headers=_headers())
- payload = response.get_json()
- assert response.headers["Access-Control-Allow-Origin"] == ORIGIN
- assert EXPOSED <= set(_exposed(response))
- assert "*" not in _exposed(response)
- assert response.headers["X-Request-Id"] == payload["requestId"]
- def test_dms_401_and_routing_404_expose_request_id(client):
- unauthorized = client.get("/api/v1/documents", headers=_headers())
- assert unauthorized.status_code == 401
- assert EXPOSED <= set(_exposed(unauthorized))
- assert (
- unauthorized.headers["X-Request-Id"]
- == unauthorized.get_json()["requestId"]
- )
- missing = client.get("/api/v1/not-implemented", headers=_headers())
- assert missing.status_code == 404
- assert EXPOSED <= set(_exposed(missing))
- assert missing.headers["X-Request-Id"] == missing.get_json()["requestId"]
- def test_expose_header_merge_is_case_insensitive_and_deduplicated():
- value = _merge_exposed_headers(
- "ETag, x-request-id, etag, CONTENT-DISPOSITION"
- )
- parts = [item.strip().lower() for item in value.split(",")]
- assert parts == ["etag", "x-request-id", "content-disposition"]
- assert len(parts) == len(set(parts))
- assert "*" not in parts
- def test_preview_and_chinese_download_expose_headers_once(
- b5_app, b5_client, token_for
- ):
- from dms.extensions import db
- from dms.models import AuditLog, Document
- token = token_for("admin")
- filename = "中文下载文件.pdf"
- created = b5_client.post(
- "/api/v1/attachments",
- data={
- "file": (io.BytesIO(b"%PDF-1.4\nCORS"), filename),
- "metadata": json.dumps(
- {
- "documentName": "B5_C1_TEST_中文下载",
- "attachmentType": "POLICY",
- "summary": "CORS响应头验证",
- "tags": ["CORS"],
- },
- ensure_ascii=False,
- ),
- },
- headers=_headers(token),
- content_type="multipart/form-data",
- ).get_json()["data"]
- preview = b5_client.get(
- f"/api/v1/documents/{created['id']}/preview",
- headers=_headers(token),
- )
- assert preview.status_code == 200
- assert EXPOSED <= set(_exposed(preview))
- assert preview.headers["Content-Disposition"].startswith("inline")
- preview.close()
- download = b5_client.get(
- f"/api/v1/documents/{created['id']}/download",
- headers=_headers(token),
- )
- disposition = download.headers["Content-Disposition"]
- assert download.status_code == 200
- assert download.headers["Access-Control-Allow-Origin"] == ORIGIN
- assert EXPOSED <= set(_exposed(download))
- assert "filename=" in disposition
- assert "filename*=UTF-8''" in disposition
- assert quote(filename, safe="") in disposition
- assert download.headers["X-Request-Id"]
- download.close()
- with b5_app.app_context():
- document = db.session.get(Document, int(created["id"]))
- audit_count = db.session.scalar(
- db.select(db.func.count(AuditLog.id)).where(
- AuditLog.target_id == document.id,
- AuditLog.action_type == "DOWNLOAD_DOCUMENT",
- )
- )
- assert document.download_count == 1
- assert audit_count == 1
- def test_ai_response_does_not_gain_dms_expose_headers(client):
- response = client.get("/api/health", headers=_headers())
- assert response.status_code == 200
- assert response.headers["Access-Control-Allow-Origin"] == ORIGIN
- assert "Access-Control-Expose-Headers" not in response.headers
|