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:
盐粒 Yanli
2026-08-04 13:14:02 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent 9d81f0da10
commit b462cb041d
50 changed files with 1091 additions and 573 deletions
@@ -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}
-1
View File
@@ -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",