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
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:
@@ -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"
|
||||
Reference in New Issue
Block a user