Files
ctms/backend/tests/test_onlyoffice_api.py
T
Cheng Zhou c68dddfc01
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
功能(文档预览):集成 ONLYOFFICE 安全只读预览与工作台体验
新增 ONLYOFFICE 配置签名、内部内容接口、容器编排与反向代理。

打通网页端和桌面端独立预览工作区,完善文档入口、布局及帮助体验。

补充桌面安全发布门禁、开发脚本、使用文档和前后端测试。
2026-07-14 14:19:17 +08:00

137 lines
6.6 KiB
Python

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()
audit = 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())
monkeypatch.setattr(onlyoffice_api, "_log_preview_open", audit)
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"
audit.assert_awaited_once()
@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())
monkeypatch.setattr(onlyoffice_api, "_log_preview_open", 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"