c68dddfc01
新增 ONLYOFFICE 配置签名、内部内容接口、容器编排与反向代理。 打通网页端和桌面端独立预览工作区,完善文档入口、布局及帮助体验。 补充桌面安全发布门禁、开发脚本、使用文档和前后端测试。
137 lines
6.6 KiB
Python
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"
|