release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-16 17:15:50 +08:00
parent 32167fba02
commit d5279b124f
393 changed files with 51630 additions and 9711 deletions
+132
View File
@@ -0,0 +1,132 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock
import uuid
import pytest
from fastapi import Response
from jose import jwt
from starlette.requests import Request
from app.api.v1 import onlyoffice as onlyoffice_api
from app.core.config import settings
from app.core.exceptions import AppException
from app.services import onlyoffice_service
@pytest.fixture(autouse=True)
def onlyoffice_settings(monkeypatch):
monkeypatch.setattr(settings, "ONLYOFFICE_ENABLED", True)
monkeypatch.setattr(settings, "ONLYOFFICE_JWT_SECRET", "office-preview-secret-that-is-long-enough")
monkeypatch.setattr(settings, "ONLYOFFICE_INSTANCE_ID", "ctms-test")
monkeypatch.setattr(settings, "ONLYOFFICE_STORAGE_BASE_URL", "http://backend:8000")
@pytest.mark.asyncio
async def test_attachment_config_reuses_permission_and_preserves_original_name(monkeypatch, tmp_path):
resource_id = uuid.uuid4()
study_id = uuid.uuid4()
entity_id = uuid.uuid4()
user = SimpleNamespace(id=uuid.uuid4(), full_name="预览用户")
source_file = tmp_path / "stored-uuid"
source_file.write_bytes(b"office")
attachment = SimpleNamespace(
id=resource_id,
study_id=study_id,
entity_type="startup_initiation",
entity_id=entity_id,
filename="研究方案最终版.DOCX",
file_path=str(source_file),
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
permission = AsyncMock()
monkeypatch.setattr(onlyoffice_api.attachment_crud, "get_attachment", AsyncMock(return_value=attachment))
monkeypatch.setattr(onlyoffice_api, "_ensure_study_exists", AsyncMock())
monkeypatch.setattr(onlyoffice_api, "_ensure_attachment_permission", permission)
monkeypatch.setattr(onlyoffice_api.onlyoffice_service, "ensure_onlyoffice_available", AsyncMock())
response = Response()
db = object()
result = await onlyoffice_api.get_attachment_preview_config(resource_id, response, db, user)
permission.assert_awaited_once_with(db, study_id, "startup_initiation", entity_id, "read", user)
assert result.file_name == "研究方案最终版.DOCX"
assert result.config["document"]["title"] == "研究方案最终版.DOCX"
assert response.headers["Cache-Control"] == "no-store"
@pytest.mark.asyncio
async def test_attachment_permission_failure_does_not_issue_config(monkeypatch, tmp_path):
source_file = tmp_path / "source.xlsx"
source_file.write_bytes(b"office")
attachment = SimpleNamespace(
id=uuid.uuid4(), study_id=uuid.uuid4(), entity_type="ethics", entity_id=uuid.uuid4(),
filename="数据.xlsx", file_path=str(source_file),
)
denied = AppException(code="FORBIDDEN", message="权限不足", status_code=403)
monkeypatch.setattr(onlyoffice_api.attachment_crud, "get_attachment", AsyncMock(return_value=attachment))
monkeypatch.setattr(onlyoffice_api, "_ensure_study_exists", AsyncMock())
monkeypatch.setattr(onlyoffice_api, "_ensure_attachment_permission", AsyncMock(side_effect=denied))
health = AsyncMock()
monkeypatch.setattr(onlyoffice_api.onlyoffice_service, "ensure_onlyoffice_available", health)
with pytest.raises(AppException) as error:
await onlyoffice_api.get_attachment_preview_config(
attachment.id, Response(), object(), SimpleNamespace(id=uuid.uuid4(), full_name="无权限用户")
)
assert error.value.status_code == 403
health.assert_not_awaited()
@pytest.mark.asyncio
async def test_version_config_reuses_document_access_and_version_hash(monkeypatch, tmp_path):
source_file = tmp_path / "uuid-storage"
source_file.write_bytes(b"office")
version = SimpleNamespace(
id=uuid.uuid4(), document_id=uuid.uuid4(), file_uri=str(source_file), original_filename="统计表.xlsx",
file_hash="sha256-current", mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
document = SimpleNamespace(id=version.document_id, trial_id=uuid.uuid4())
user = SimpleNamespace(id=uuid.uuid4(), full_name="管理员")
access = AsyncMock()
monkeypatch.setattr(onlyoffice_api.version_crud, "get", AsyncMock(return_value=version))
monkeypatch.setattr(onlyoffice_api.document_crud, "get", AsyncMock(return_value=document))
monkeypatch.setattr(onlyoffice_api.document_service, "_ensure_study_access", access)
monkeypatch.setattr(onlyoffice_api.onlyoffice_service, "ensure_onlyoffice_available", AsyncMock())
result = await onlyoffice_api.get_version_preview_config(version.id, Response(), object(), user)
assert access.await_args.args[1:] == (document.trial_id, user)
assert access.await_args.kwargs == {"action": "view"}
assert result.file_name == "统计表.xlsx"
assert result.config["document"]["key"] == onlyoffice_service.onlyoffice_document_key(
"version", version.id, file_hash="sha256-current"
)
@pytest.mark.asyncio
async def test_internal_attachment_requires_url_bound_jwt_and_returns_original_name(monkeypatch, tmp_path):
source_file = tmp_path / "uuid-storage-name"
source_file.write_bytes(b"office")
attachment = SimpleNamespace(
id=uuid.uuid4(), filename="中文原始文件名.xlsx", file_path=str(source_file),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
expected_url = onlyoffice_service.onlyoffice_content_url("attachment", attachment.id)
token = jwt.encode({"payload": {"url": expected_url}}, settings.ONLYOFFICE_JWT_SECRET, algorithm="HS256")
request = Request({
"type": "http", "method": "GET", "path": f"/internal/onlyoffice/attachments/{attachment.id}/content",
"headers": [(b"authorizationjwt", token.encode())],
})
monkeypatch.setattr(onlyoffice_api.attachment_crud, "get_attachment", AsyncMock(return_value=attachment))
response = await onlyoffice_api.get_internal_attachment_content(attachment.id, request, object())
assert "uuid-storage-name" not in response.headers["content-disposition"]
assert "UTF-8''" in response.headers["content-disposition"]
assert response.media_type == attachment.content_type
missing_token_request = Request({"type": "http", "method": "GET", "path": "/", "headers": []})
with pytest.raises(AppException) as error:
await onlyoffice_api.get_internal_attachment_content(attachment.id, missing_token_request, object())
assert error.value.code == "ONLYOFFICE_SOURCE_UNAUTHORIZED"