dify/api/tests/unit_tests/controllers/files/test_image_preview.py
Myshkin451 c903e93e8b
fix: preserve file preview content type (#37211)
Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-14 07:45:21 +00:00

288 lines
8.6 KiB
Python

import types
from inspect import unwrap
from unittest.mock import patch
import pytest
from werkzeug.exceptions import NotFound
import controllers.files.image_preview as module
@pytest.fixture(autouse=True)
def mock_db():
"""
Replace Flask-SQLAlchemy db with a plain object
to avoid touching Flask app context entirely.
"""
fake_db = types.SimpleNamespace(engine=object())
module.db = fake_db
class DummyUploadFile:
def __init__(self, mime_type="text/plain", size=10, name="test.txt", extension="txt"):
self.mime_type = mime_type
self.size = size
self.name = name
self.extension = extension
def fake_request(args: dict):
"""Return a fake request object (NOT a Flask LocalProxy)."""
return types.SimpleNamespace(args=types.SimpleNamespace(to_dict=lambda flat=True: args))
class TestImagePreviewApi:
@patch.object(module, "FileService")
def test_success(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
}
)
generator = iter([b"img"])
mock_file_service.return_value.get_image_preview.return_value = (
generator,
"image/png",
)
api = module.ImagePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.mimetype == "image/png"
@patch.object(module, "FileService")
def test_unsupported_file_type(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
}
)
mock_file_service.return_value.get_image_preview.side_effect = (
module.services.errors.file.UnsupportedFileTypeError()
)
api = module.ImagePreviewApi()
get_fn = unwrap(api.get)
with pytest.raises(module.UnsupportedFileTypeError):
get_fn("file-id")
class TestFilePreviewApi:
@patch.object(module, "enforce_download_for_html")
@patch.object(module, "FileService")
def test_inline_preview_uses_upload_file_mimetype(self, mock_file_service, mock_enforce):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
generator = iter([b"data"])
upload_file = DummyUploadFile(
mime_type="application/pdf",
size=100,
name="doc.pdf",
extension="pdf",
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.mimetype == "application/pdf"
assert response.headers["Content-Type"] == "application/pdf"
assert response.headers["Content-Length"] == "100"
assert "Accept-Ranges" not in response.headers
mock_enforce.assert_called_once()
@pytest.mark.parametrize(
("mime_type", "name", "extension"),
[
("Image/SVG+XML; charset=UTF-8", "image.png", "png"),
("image/png", "image.SVG", "png"),
("image/png", "image.png", ".SVG"),
],
ids=("mime-type", "filename", "extension"),
)
@patch.object(module, "FileService")
def test_svg_preview_forces_download(self, mock_file_service, mime_type, name, extension):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
generator = iter([b"<svg></svg>"])
upload_file = DummyUploadFile(
mime_type=mime_type,
size=11,
name=name,
extension=extension,
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.headers["Content-Disposition"].startswith("attachment")
assert response.headers["Content-Type"] == "application/octet-stream"
assert response.headers["X-Content-Type-Options"] == "nosniff"
@patch.object(module, "FileService")
def test_html_preview_still_forces_download(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
generator = iter([b"<script>alert(1)</script>"])
upload_file = DummyUploadFile(
mime_type="text/html",
size=25,
name="unsafe.html",
extension="html",
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.headers["Content-Disposition"].startswith("attachment")
assert response.headers["Content-Type"] == "application/octet-stream"
assert response.headers["X-Content-Type-Options"] == "nosniff"
@patch.object(module, "enforce_download_for_html")
@patch.object(module, "FileService")
def test_as_attachment(self, mock_file_service, mock_enforce):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": True,
}
)
generator = iter([b"data"])
upload_file = DummyUploadFile(
mime_type="application/pdf",
name="doc.pdf",
extension="pdf",
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.headers["Content-Disposition"].startswith("attachment")
assert response.headers["Content-Type"] == "application/octet-stream"
mock_enforce.assert_called_once()
@patch.object(module, "FileService")
def test_unsupported_file_type(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
mock_file_service.return_value.get_file_generator_by_file_id.side_effect = (
module.services.errors.file.UnsupportedFileTypeError()
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
with pytest.raises(module.UnsupportedFileTypeError):
get_fn("file-id")
class TestWorkspaceWebappLogoApi:
@patch.object(module, "FileService")
@patch.object(module.TenantService, "get_custom_config")
def test_success(self, mock_config, mock_file_service):
mock_config.return_value = {"replace_webapp_logo": "logo-id"}
generator = iter([b"logo"])
mock_file_service.return_value.get_public_image_preview.return_value = (
generator,
"image/png",
)
api = module.WorkspaceWebappLogoApi()
get_fn = unwrap(api.get)
response = get_fn("workspace-id")
assert response.mimetype == "image/png"
@patch.object(module.TenantService, "get_custom_config")
def test_logo_not_configured(self, mock_config):
mock_config.return_value = {}
api = module.WorkspaceWebappLogoApi()
get_fn = unwrap(api.get)
with pytest.raises(NotFound):
get_fn("workspace-id")
@patch.object(module, "FileService")
@patch.object(module.TenantService, "get_custom_config")
def test_unsupported_file_type(self, mock_config, mock_file_service):
mock_config.return_value = {"replace_webapp_logo": "logo-id"}
mock_file_service.return_value.get_public_image_preview.side_effect = (
module.services.errors.file.UnsupportedFileTypeError()
)
api = module.WorkspaceWebappLogoApi()
get_fn = unwrap(api.get)
with pytest.raises(module.UnsupportedFileTypeError):
get_fn("workspace-id")