mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
feat(agent): separate CLI file URL audiences (#39952)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
autofix-ci[bot]
parent
9d81f0da10
commit
b462cb041d
@@ -202,6 +202,16 @@ def test_internal_files_url_prefers_explicit_value(monkeypatch: pytest.MonkeyPat
|
||||
assert config.INTERNAL_FILES_URL == "http://files-internal:5001"
|
||||
|
||||
|
||||
def test_empty_files_url_overrides_console_api_url_for_relative_browser_uris(monkeypatch: pytest.MonkeyPatch):
|
||||
_clear_environment(monkeypatch)
|
||||
monkeypatch.setenv("FILES_URL", "")
|
||||
monkeypatch.setenv("CONSOLE_API_URL", "http://api:5001")
|
||||
|
||||
config = DifyConfig(_env_file=None)
|
||||
|
||||
assert config.FILES_URL == ""
|
||||
|
||||
|
||||
# NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected.
|
||||
# This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`.
|
||||
def test_flask_configs(monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
@@ -263,11 +263,12 @@ class TestPluginUploadFileRequestApi:
|
||||
assert hasattr(api_instance, "post")
|
||||
assert callable(api_instance.post)
|
||||
|
||||
@patch("controllers.inner_api.plugin.plugin.get_signed_file_url_for_plugin")
|
||||
def test_post_returns_signed_url(self, mock_get_url, api_instance, app: Flask):
|
||||
@patch("controllers.inner_api.plugin.plugin.get_signed_file_uri_for_plugin")
|
||||
def test_post_returns_signed_url(self, mock_get_uri, api_instance, app: Flask, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Test that post() generates a signed URL and returns it"""
|
||||
# Arrange
|
||||
mock_get_url.return_value = "https://storage.example.com/signed-upload-url"
|
||||
mock_get_uri.return_value = "/files/upload/for-plugin?sign=1"
|
||||
monkeypatch.setattr(plugin_module.dify_config, "INTERNAL_FILES_URL", "http://api:5001")
|
||||
mock_tenant = MagicMock()
|
||||
mock_tenant.id = "tenant-id"
|
||||
mock_user = MagicMock()
|
||||
@@ -282,14 +283,14 @@ class TestPluginUploadFileRequestApi:
|
||||
result = raw_post(api_instance, user_model=mock_user, tenant_model=mock_tenant, payload=mock_payload)
|
||||
|
||||
# Assert
|
||||
mock_get_url.assert_called_once_with(
|
||||
mock_get_uri.assert_called_once_with(
|
||||
filename="test.pdf",
|
||||
mimetype="application/pdf",
|
||||
tenant_id="tenant-id",
|
||||
user_id="user-id",
|
||||
conversation_id="conversation-id",
|
||||
)
|
||||
assert result["data"]["url"] == "https://storage.example.com/signed-upload-url"
|
||||
assert result["data"]["url"] == "http://api:5001/files/upload/for-plugin?sign=1"
|
||||
|
||||
|
||||
class TestPluginDownloadFileRequestApi:
|
||||
@@ -304,6 +305,13 @@ class TestPluginDownloadFileRequestApi:
|
||||
assert callable(api_instance.post)
|
||||
|
||||
@pytest.mark.parametrize("sqlite_session", [(Tenant,)], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("for_external", "expected_url"),
|
||||
[
|
||||
(True, "https://files.example.com/files/tools/report.pdf?sign=1"),
|
||||
(False, "http://api:5001/files/tools/report.pdf?sign=1"),
|
||||
],
|
||||
)
|
||||
@patch("controllers.inner_api.plugin.plugin.FileRequestService")
|
||||
def test_post_returns_signed_download_url(
|
||||
self,
|
||||
@@ -312,6 +320,8 @@ class TestPluginDownloadFileRequestApi:
|
||||
app: Flask,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
sqlite_session: Session,
|
||||
for_external: bool,
|
||||
expected_url: str,
|
||||
):
|
||||
tenant = Tenant(
|
||||
name="Plugin Tenant",
|
||||
@@ -324,18 +334,20 @@ class TestPluginDownloadFileRequestApi:
|
||||
sqlite_session.commit()
|
||||
monkeypatch.setattr(plugin_module.db, "session", sqlite_session)
|
||||
mock_service = mock_service_cls.return_value
|
||||
mock_service.request_download_url.return_value = MagicMock(
|
||||
mock_service.request_download.return_value = MagicMock(
|
||||
filename="report.pdf",
|
||||
mime_type="application/pdf",
|
||||
size=123,
|
||||
download_url="https://files.example.com/download",
|
||||
download_uri="/files/tools/report.pdf?sign=1",
|
||||
)
|
||||
monkeypatch.setattr(plugin_module.dify_config, "FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr(plugin_module.dify_config, "INTERNAL_FILES_URL", "http://api:5001")
|
||||
mock_payload = MagicMock()
|
||||
mock_payload.tenant_id = tenant.id
|
||||
mock_payload.user_id = "user-id"
|
||||
mock_payload.user_from = "account"
|
||||
mock_payload.invoke_from = "debugger"
|
||||
mock_payload.for_external = False
|
||||
mock_payload.for_external = for_external
|
||||
reference = build_file_reference(record_id="tool-file-1")
|
||||
mock_payload.file.model_dump.return_value = {
|
||||
"transfer_method": "tool_file",
|
||||
@@ -345,19 +357,18 @@ class TestPluginDownloadFileRequestApi:
|
||||
raw_post = _extract_raw_post(PluginDownloadFileRequestApi)
|
||||
result = raw_post(api_instance, payload=mock_payload)
|
||||
|
||||
mock_service.request_download_url.assert_called_once_with(
|
||||
mock_service.request_download.assert_called_once_with(
|
||||
tenant_id=tenant.id,
|
||||
user_id="user-id",
|
||||
user_from="account",
|
||||
invoke_from="debugger",
|
||||
file_mapping={"transfer_method": "tool_file", "reference": reference},
|
||||
for_external=False,
|
||||
)
|
||||
assert result["data"] == {
|
||||
"filename": "report.pdf",
|
||||
"mime_type": "application/pdf",
|
||||
"size": 123,
|
||||
"download_url": "https://files.example.com/download",
|
||||
"download_url": expected_url,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from controllers.inner_api.agent.files import AgentFileDownloadRequestApi, AgentFileUploadRequestApi
|
||||
from core.workflow.file_reference import build_file_reference
|
||||
from services.file_request_service import DownloadFileRequestResult
|
||||
|
||||
MODULE = "controllers.inner_api.agent.files"
|
||||
|
||||
|
||||
def _raw[R](method: Callable[..., R]) -> Callable[..., R]:
|
||||
return cast(Callable[..., R], inspect.unwrap(method))
|
||||
|
||||
|
||||
def test_upload_request_returns_origin_free_uri(app: Flask) -> None:
|
||||
payload = {
|
||||
"tenant_id": "tenant-1",
|
||||
"user_id": "execution-user-1",
|
||||
"filename": "report.pdf",
|
||||
"mimetype": "application/pdf",
|
||||
"conversation_id": "conversation-1",
|
||||
}
|
||||
tenant = SimpleNamespace(id="tenant-1")
|
||||
user = SimpleNamespace(id="canonical-end-user-1")
|
||||
session = MagicMock()
|
||||
with app.test_request_context("/", method="POST", json=payload):
|
||||
with (
|
||||
patch(f"{MODULE}.TenantService") as tenant_service,
|
||||
patch(f"{MODULE}.get_user", return_value=user),
|
||||
patch(f"{MODULE}.get_signed_file_uri_for_plugin", return_value="/files/upload/for-plugin?sign=1") as sign,
|
||||
):
|
||||
tenant_service.get_tenant_by_id.return_value = tenant
|
||||
response = _raw(AgentFileUploadRequestApi.post)(AgentFileUploadRequestApi(), session)
|
||||
|
||||
assert response == {"upload_uri": "/files/upload/for-plugin?sign=1"}
|
||||
tenant_service.get_tenant_by_id.assert_called_once_with("tenant-1", session=session)
|
||||
sign.assert_called_once_with(
|
||||
filename="report.pdf",
|
||||
mimetype="application/pdf",
|
||||
tenant_id="tenant-1",
|
||||
user_id="canonical-end-user-1",
|
||||
conversation_id="conversation-1",
|
||||
)
|
||||
|
||||
|
||||
def test_download_request_returns_origin_free_uri_for_sandbox(app: Flask) -> None:
|
||||
reference = build_file_reference(record_id="tool-file-1")
|
||||
payload = {
|
||||
"tenant_id": "tenant-1",
|
||||
"user_id": "user-1",
|
||||
"user_from": "account",
|
||||
"invoke_from": "debugger",
|
||||
"file": {"transfer_method": "tool_file", "reference": reference},
|
||||
"for_frontend": False,
|
||||
}
|
||||
session = MagicMock()
|
||||
with app.test_request_context("/", method="POST", json=payload):
|
||||
with (
|
||||
patch(f"{MODULE}.TenantService") as tenant_service,
|
||||
patch(f"{MODULE}.FileRequestService") as service,
|
||||
):
|
||||
tenant_service.get_tenant_by_id.return_value = MagicMock()
|
||||
service.return_value.request_download.return_value = DownloadFileRequestResult(
|
||||
filename="report.pdf",
|
||||
mime_type="application/pdf",
|
||||
size=123,
|
||||
download_uri="/files/tools/tool-file-1.pdf?sign=1",
|
||||
)
|
||||
response = _raw(AgentFileDownloadRequestApi.post)(AgentFileDownloadRequestApi(), session)
|
||||
|
||||
assert response == {
|
||||
"filename": "report.pdf",
|
||||
"mime_type": "application/pdf",
|
||||
"size": 123,
|
||||
"download_uri": "/files/tools/tool-file-1.pdf?sign=1",
|
||||
}
|
||||
service.return_value.request_download.assert_called_once_with(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
user_from="account",
|
||||
invoke_from="debugger",
|
||||
file_mapping={"transfer_method": "tool_file", "reference": reference},
|
||||
)
|
||||
|
||||
|
||||
def test_download_request_binds_frontend_url(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
reference = build_file_reference(record_id="tool-file-1")
|
||||
payload = {
|
||||
"tenant_id": "tenant-1",
|
||||
"user_id": "user-1",
|
||||
"user_from": "account",
|
||||
"invoke_from": "debugger",
|
||||
"file": {"transfer_method": "tool_file", "reference": reference},
|
||||
"for_frontend": True,
|
||||
}
|
||||
monkeypatch.setattr(f"{MODULE}.dify_config.FILES_URL", "https://files.example.com")
|
||||
session = MagicMock()
|
||||
with app.test_request_context("/", method="POST", json=payload):
|
||||
with (
|
||||
patch(f"{MODULE}.TenantService") as tenant_service,
|
||||
patch(f"{MODULE}.FileRequestService") as service,
|
||||
):
|
||||
tenant_service.get_tenant_by_id.return_value = MagicMock()
|
||||
service.return_value.request_download.return_value = DownloadFileRequestResult(
|
||||
filename="report.pdf",
|
||||
mime_type="application/pdf",
|
||||
size=123,
|
||||
download_uri="/files/tools/tool-file-1.pdf?sign=1",
|
||||
)
|
||||
response = _raw(AgentFileDownloadRequestApi.post)(AgentFileDownloadRequestApi(), session)
|
||||
|
||||
assert response["download_uri"] == "https://files.example.com/files/tools/tool-file-1.pdf?sign=1"
|
||||
@@ -141,8 +141,9 @@ def test_resolve_file_url_requires_extension_for_tool_files() -> None:
|
||||
def test_resolve_file_url_uses_tool_signatures_for_tool_and_datasource_files(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
sign_tool_file = MagicMock(return_value="https://signed.example.com/file")
|
||||
monkeypatch.setattr(file_runtime, "sign_tool_file", sign_tool_file)
|
||||
sign_tool_file_uri = MagicMock(return_value="/files/signed")
|
||||
monkeypatch.setattr(file_runtime, "sign_tool_file_uri", sign_tool_file_uri)
|
||||
monkeypatch.setattr(file_runtime.dify_config, "FILES_URL", "https://files.example.com")
|
||||
runtime = _build_runtime()
|
||||
|
||||
tool_file = _build_file(
|
||||
@@ -156,9 +157,35 @@ def test_resolve_file_url_uses_tool_signatures_for_tool_and_datasource_files(
|
||||
extension=".png",
|
||||
)
|
||||
|
||||
assert runtime.resolve_file_url(file=tool_file) == "https://signed.example.com/file"
|
||||
assert runtime.resolve_file_url(file=datasource_file) == "https://signed.example.com/file"
|
||||
assert sign_tool_file.call_count == 2
|
||||
assert runtime.resolve_file_url(file=tool_file) == "https://files.example.com/files/signed"
|
||||
assert runtime.resolve_file_url(file=datasource_file) == "https://files.example.com/files/signed"
|
||||
assert sign_tool_file_uri.call_count == 2
|
||||
|
||||
|
||||
def test_resolve_file_uri_keeps_dify_owned_file_origin_free(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
sign_tool_file_uri = MagicMock(return_value="/files/tools/tool-file-id.png?sign=1")
|
||||
monkeypatch.setattr(file_runtime, "sign_tool_file_uri", sign_tool_file_uri)
|
||||
runtime = _build_runtime()
|
||||
file = _build_file(
|
||||
transfer_method=FileTransferMethod.TOOL_FILE,
|
||||
reference=build_file_reference(record_id="tool-file-id"),
|
||||
extension=".png",
|
||||
)
|
||||
|
||||
assert runtime.resolve_file_uri(file=file) == "/files/tools/tool-file-id.png?sign=1"
|
||||
|
||||
|
||||
def test_resolve_file_url_returns_relative_uri_when_files_url_is_empty(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(file_runtime, "sign_tool_file_uri", lambda **_: "/files/tools/tool-file-id.png?sign=1")
|
||||
monkeypatch.setattr(file_runtime.dify_config, "FILES_URL", "")
|
||||
runtime = _build_runtime()
|
||||
file = _build_file(
|
||||
transfer_method=FileTransferMethod.TOOL_FILE,
|
||||
reference=build_file_reference(record_id="tool-file-id"),
|
||||
extension=".png",
|
||||
)
|
||||
|
||||
assert runtime.resolve_file_url(file=file, for_external=True) == "/files/tools/tool-file-id.png?sign=1"
|
||||
|
||||
|
||||
def test_resolve_upload_file_url_signs_internal_urls_and_supports_attachments(
|
||||
|
||||
@@ -7,14 +7,40 @@ from urllib.parse import parse_qs, urlparse
|
||||
import pytest
|
||||
|
||||
from core.tools.signature import (
|
||||
get_signed_file_url_for_plugin,
|
||||
bind_file_uri,
|
||||
get_signed_file_uri_for_plugin,
|
||||
sign_tool_file,
|
||||
sign_tool_file_uri,
|
||||
sign_upload_file_preview_url,
|
||||
verify_plugin_file_signature,
|
||||
verify_tool_file_signature,
|
||||
)
|
||||
|
||||
|
||||
def test_bind_file_uri_uses_selected_base_and_preserves_remote_url() -> None:
|
||||
uri = "/files/tools/tool-file-id.png?sign=1"
|
||||
|
||||
assert bind_file_uri(uri, "https://files.example.com") == f"https://files.example.com{uri}"
|
||||
assert bind_file_uri(uri, "") == uri
|
||||
assert bind_file_uri("https://remote.example.com/report.pdf", "https://files.example.com") == (
|
||||
"https://remote.example.com/report.pdf"
|
||||
)
|
||||
|
||||
|
||||
def test_sign_tool_file_uri_has_no_origin(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x08" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
|
||||
uri = sign_tool_file_uri("tool-file-id", ".png")
|
||||
parsed = urlparse(uri)
|
||||
|
||||
assert parsed.scheme == ""
|
||||
assert parsed.netloc == ""
|
||||
assert parsed.path == "/files/tools/tool-file-id.png"
|
||||
assert parse_qs(parsed.query)["timestamp"] == ["1700000000"]
|
||||
|
||||
|
||||
def test_sign_tool_file_and_verify_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x01" * 16)
|
||||
@@ -125,25 +151,23 @@ def test_sign_upload_file_preview_url_ignores_internal_files_url(monkeypatch: py
|
||||
assert query["sign"][0]
|
||||
|
||||
|
||||
def test_get_signed_file_url_for_plugin_and_verify_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_get_signed_file_uri_for_plugin_and_verify_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x06" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "https://internal.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 60)
|
||||
|
||||
url = get_signed_file_url_for_plugin(
|
||||
uri = get_signed_file_uri_for_plugin(
|
||||
filename="report.pdf",
|
||||
mimetype="application/pdf",
|
||||
tenant_id="tenant-id",
|
||||
user_id="user-id",
|
||||
conversation_id="conversation-id",
|
||||
)
|
||||
parsed = urlparse(url)
|
||||
parsed = urlparse(uri)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
assert parsed.netloc == "internal.example.com"
|
||||
assert parsed.netloc == ""
|
||||
assert parsed.path == "/files/upload/for-plugin"
|
||||
assert query["tenant_id"] == ["tenant-id"]
|
||||
assert query["user_id"] == ["user-id"]
|
||||
@@ -167,17 +191,15 @@ def test_verify_plugin_file_signature_rejects_invalid_signatures(monkeypatch: py
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x07" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 30)
|
||||
|
||||
url = get_signed_file_url_for_plugin(
|
||||
uri = get_signed_file_uri_for_plugin(
|
||||
filename="report.pdf",
|
||||
mimetype="application/pdf",
|
||||
tenant_id="tenant-id",
|
||||
user_id="user-id",
|
||||
)
|
||||
query = parse_qs(urlparse(url).query)
|
||||
query = parse_qs(urlparse(uri).query)
|
||||
|
||||
assert (
|
||||
verify_plugin_file_signature(
|
||||
|
||||
@@ -432,7 +432,7 @@ def test_builds_workflow_run_request_with_file_output_schema_and_reserved_metada
|
||||
assert "never invent the `reference` value" in output_description
|
||||
assert "Do not call `final_output` before the upload command succeeds" in output_description
|
||||
assert "accepted file-mapping shape and the returned `reference`" in output_description
|
||||
assert "include the returned `download_url` in that reply" in output_description
|
||||
assert "include the returned `public_download_url` in that reply" in output_description
|
||||
assert output_schema["properties"]["confidence"]["type"] == "number"
|
||||
assert output_schema["required"] == ["report"]
|
||||
assert layers[DIFY_AGENT_MODEL_LAYER_ID]["config"]["model_settings"] == {"temperature": 0.2}
|
||||
|
||||
@@ -820,7 +820,6 @@ project-excludes = [
|
||||
"services/test_agent_app_sandbox_service.py",
|
||||
"services/test_agent_config_service.py",
|
||||
"services/test_agent_drive_service.py",
|
||||
"services/test_agent_file_request_service.py",
|
||||
"services/test_annotation_service.py",
|
||||
"services/test_api_token_service.py",
|
||||
"services/test_app_generate_service.py",
|
||||
|
||||
@@ -246,7 +246,7 @@ def test_node_job_resolver_resolves_each_kind(node_job: WorkflowNodeJobConfig):
|
||||
"Read START/tenders and produce qna_report (file output; create the file locally, run "
|
||||
"`dify-agent file upload <path>`, then set final_output.qna_report to a `tool_file` mapping "
|
||||
"using the returned `reference`; if replying to the user in natural language, use the returned "
|
||||
"`download_url`; do not call final_output before upload succeeds, and do not use the local path, "
|
||||
"`public_download_url`; do not call final_output before upload succeeds, and do not use the local path, "
|
||||
"filename, URL, or a synthesized dify-file-ref as the reference); "
|
||||
"if unsure contact EMAIL · David Hayes."
|
||||
)
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
"""Unit tests for the Agent Files download-request service (ENG-592)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from services.agent_file_request_service import AgentFileDownloadRequestService, FileDownloadRequestError
|
||||
|
||||
_MOD = "services.agent_file_request_service"
|
||||
|
||||
|
||||
def _fake_file() -> SimpleNamespace:
|
||||
return SimpleNamespace(filename="report.pdf", mime_type="application/pdf", size=12)
|
||||
|
||||
|
||||
def test_resolve_returns_metadata_and_internal_url():
|
||||
with (
|
||||
patch(f"{_MOD}.file_factory.build_from_mapping", return_value=_fake_file()) as build,
|
||||
patch(f"{_MOD}.DifyWorkflowFileRuntime") as runtime_cls,
|
||||
):
|
||||
runtime_cls.return_value.resolve_file_url.return_value = "http://internal/files/x?sign=1"
|
||||
data = AgentFileDownloadRequestService.resolve(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
user_from="account",
|
||||
invoke_from="service-api",
|
||||
file_mapping={"transfer_method": "tool_file", "reference": "tool-file-1"},
|
||||
)
|
||||
|
||||
assert data == {
|
||||
"filename": "report.pdf",
|
||||
"mime_type": "application/pdf",
|
||||
"size": 12,
|
||||
"download_url": "http://internal/files/x?sign=1",
|
||||
}
|
||||
assert build.call_args.kwargs["tenant_id"] == "tenant-1"
|
||||
# Sandbox/agent backend consumes the URL -> must be internal, not external.
|
||||
assert runtime_cls.return_value.resolve_file_url.call_args.kwargs["for_external"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_from", "invoke_from", "code"),
|
||||
[
|
||||
("bogus", "service-api", "invalid_access_context"),
|
||||
("account", "not-a-source", "invalid_access_context"),
|
||||
],
|
||||
)
|
||||
def test_invalid_access_context_rejected(user_from: str, invoke_from: str, code: str):
|
||||
with pytest.raises(FileDownloadRequestError) as exc_info:
|
||||
AgentFileDownloadRequestService.resolve(
|
||||
tenant_id="t",
|
||||
user_id="u",
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
file_mapping={"transfer_method": "tool_file", "reference": "x"},
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.code == code
|
||||
|
||||
|
||||
def test_missing_transfer_method_rejected():
|
||||
with pytest.raises(FileDownloadRequestError) as exc_info:
|
||||
AgentFileDownloadRequestService.resolve(
|
||||
tenant_id="t",
|
||||
user_id="u",
|
||||
user_from="account",
|
||||
invoke_from="service-api",
|
||||
file_mapping={},
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.code == "invalid_file_mapping"
|
||||
|
||||
|
||||
def test_inaccessible_file_maps_to_404():
|
||||
with patch(f"{_MOD}.file_factory.build_from_mapping", side_effect=ValueError("ToolFile x not found")):
|
||||
with pytest.raises(FileDownloadRequestError) as exc_info:
|
||||
AgentFileDownloadRequestService.resolve(
|
||||
tenant_id="t",
|
||||
user_id="u",
|
||||
user_from="end-user",
|
||||
invoke_from="web-app",
|
||||
file_mapping={"transfer_method": "tool_file", "reference": "x"},
|
||||
)
|
||||
assert exc_info.value.status_code == 404
|
||||
assert exc_info.value.code == "file_not_accessible"
|
||||
|
||||
|
||||
def test_unresolved_url_maps_to_502():
|
||||
with (
|
||||
patch(f"{_MOD}.file_factory.build_from_mapping", return_value=_fake_file()),
|
||||
patch(f"{_MOD}.DifyWorkflowFileRuntime") as runtime_cls,
|
||||
):
|
||||
runtime_cls.return_value.resolve_file_url.return_value = None
|
||||
with pytest.raises(FileDownloadRequestError) as exc_info:
|
||||
AgentFileDownloadRequestService.resolve(
|
||||
tenant_id="t",
|
||||
user_id="u",
|
||||
user_from="account",
|
||||
invoke_from="service-api",
|
||||
file_mapping={"transfer_method": "tool_file", "reference": "x"},
|
||||
)
|
||||
assert exc_info.value.status_code == 502
|
||||
@@ -15,7 +15,7 @@ from services.file_request_service import FileRequestService
|
||||
("end-user", "service-api", UserFrom.END_USER, InvokeFrom.SERVICE_API),
|
||||
],
|
||||
)
|
||||
def test_request_download_url_builds_file_under_bound_scope(
|
||||
def test_request_download_builds_file_under_bound_scope(
|
||||
user_from: UserFrom | str,
|
||||
invoke_from: InvokeFrom | str,
|
||||
expected_user_from: UserFrom,
|
||||
@@ -29,12 +29,9 @@ def test_request_download_url_builds_file_under_bound_scope(
|
||||
with (
|
||||
patch("services.file_request_service.bind_file_access_scope", return_value=nullcontext()) as bind_scope,
|
||||
patch.object(service, "_build_file", return_value=fake_file) as build_file,
|
||||
patch(
|
||||
"services.file_request_service.file_helpers.resolve_file_url",
|
||||
return_value="https://files.example.com/x",
|
||||
) as resolve_file_url,
|
||||
patch.object(service._runtime, "resolve_file_uri", return_value="/files/tools/x?sign=1") as resolve_file_uri,
|
||||
):
|
||||
result = service.request_download_url(
|
||||
result = service.request_download(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
user_from=user_from,
|
||||
@@ -52,48 +49,23 @@ def test_request_download_url_builds_file_under_bound_scope(
|
||||
build_file.assert_called_once_with(
|
||||
mapping={"transfer_method": "tool_file", "reference": reference}, tenant_id="tenant-1"
|
||||
)
|
||||
resolve_file_url.assert_called_once_with(fake_file, for_external=True)
|
||||
resolve_file_uri.assert_called_once_with(file=fake_file)
|
||||
assert result.filename == "report.pdf"
|
||||
assert result.mime_type == "application/pdf"
|
||||
assert result.size == 123
|
||||
assert result.download_url == "https://files.example.com/x"
|
||||
assert result.download_uri == "/files/tools/x?sign=1"
|
||||
|
||||
|
||||
def test_request_download_url_supports_internal_download_urls() -> None:
|
||||
fake_file = MagicMock(filename="report.pdf", mime_type="application/pdf", size=123)
|
||||
service = FileRequestService(access_controller=MagicMock())
|
||||
|
||||
with (
|
||||
patch("services.file_request_service.bind_file_access_scope", return_value=nullcontext()),
|
||||
patch.object(service, "_build_file", return_value=fake_file),
|
||||
patch(
|
||||
"services.file_request_service.file_helpers.resolve_file_url",
|
||||
return_value="http://internal-files/report.pdf",
|
||||
) as resolve_file_url,
|
||||
):
|
||||
result = service.request_download_url(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
user_from="account",
|
||||
invoke_from="debugger",
|
||||
file_mapping={"transfer_method": "tool_file", "reference": "dify-file-ref:tool-file-1"},
|
||||
for_external=False,
|
||||
)
|
||||
|
||||
resolve_file_url.assert_called_once_with(fake_file, for_external=False)
|
||||
assert result.download_url == "http://internal-files/report.pdf"
|
||||
|
||||
|
||||
def test_request_download_url_rejects_unsupported_files() -> None:
|
||||
def test_request_download_rejects_unsupported_files() -> None:
|
||||
service = FileRequestService(access_controller=MagicMock())
|
||||
|
||||
with (
|
||||
patch("services.file_request_service.bind_file_access_scope", return_value=nullcontext()),
|
||||
patch.object(service, "_build_file", return_value=MagicMock(filename="report.pdf", mime_type=None, size=1)),
|
||||
patch("services.file_request_service.file_helpers.resolve_file_url", return_value=None),
|
||||
patch.object(service._runtime, "resolve_file_uri", return_value=None),
|
||||
):
|
||||
with pytest.raises(ValueError, match="file does not support signed download"):
|
||||
service.request_download_url(
|
||||
service.request_download(
|
||||
tenant_id="tenant-1",
|
||||
user_id="user-1",
|
||||
user_from="account",
|
||||
|
||||
Reference in New Issue
Block a user