| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- """Unit tests for fastapi_utils file_handler module."""
- import sys
- from pathlib import Path
- import pytest
- from agency_swarm.integrations.fastapi_utils.file_handler import upload_from_urls
- @pytest.mark.asyncio
- async def test_upload_from_urls_rejects_unsupported_sources() -> None:
- """Unsupported or relative sources should raise a clear validation error."""
- invalid_sources = [
- "s3://bucket/key",
- "ftp://example.com/file.pdf",
- "uploads/file.pdf",
- "./uploads/file.pdf",
- ]
- for source in invalid_sources:
- with pytest.raises(ValueError, match="Unsupported URL scheme"):
- await upload_from_urls({"file.pdf": source})
- @pytest.mark.asyncio
- @pytest.mark.skipif(sys.platform == "win32", reason="On Windows, // paths are treated as UNC")
- async def test_upload_from_urls_rejects_protocol_relative_on_non_windows() -> None:
- """Protocol-relative URLs should be rejected before download on non-Windows hosts."""
- protocol_relative_urls = ["//example.com/file.pdf", "//cdn.example.com/file.js"]
- for source in protocol_relative_urls:
- with pytest.raises(ValueError, match="URL scheme is required"):
- await upload_from_urls({"file.pdf": source})
- @pytest.mark.asyncio
- async def test_upload_from_urls_uploads_supported_local_sources(
- monkeypatch: pytest.MonkeyPatch,
- tmp_path: Path,
- ) -> None:
- """Absolute and file:// sources should all resolve to local uploads."""
- plain_file = tmp_path / "doc.txt"
- plain_file.write_text("hello", encoding="utf-8")
- spaced_file = tmp_path / "uri file.txt"
- spaced_file.write_text("hello", encoding="utf-8")
- async def fake_upload(path: str) -> str:
- return f"uploaded:{Path(path).name}"
- async def fake_wait(_file_id: str) -> None:
- return None
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler.upload_to_openai",
- fake_upload,
- )
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler._wait_for_file_processed",
- fake_wait,
- )
- sources = {
- "absolute": ("doc.txt", str(plain_file), {"doc.txt": "uploaded:doc.txt"}),
- "file_uri": ("doc.txt", plain_file.as_uri(), {"doc.txt": "uploaded:doc.txt"}),
- "encoded_space_uri": (
- "uri file.txt",
- spaced_file.as_uri(),
- {"uri file.txt": "uploaded:uri file.txt"},
- ),
- }
- if sys.platform != "win32":
- localhost_uri = f"file://localhost{plain_file}"
- sources["localhost_uri"] = ("doc.txt", localhost_uri, {"doc.txt": "uploaded:doc.txt"})
- for _name, (filename, source, expected) in sources.items():
- result = await upload_from_urls({filename: source}, allowed_local_dirs=[str(tmp_path)])
- assert result == expected
- @pytest.mark.asyncio
- async def test_upload_from_urls_forwards_openai_client(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
- """Request-scoped OpenAI client should be forwarded to upload and poll helpers."""
- file_path = tmp_path / "doc.txt"
- file_path.write_text("hello", encoding="utf-8")
- client_sentinel = object()
- seen: list[object] = []
- async def fake_upload(path: str, openai_client: object | None = None) -> str:
- del path
- seen.append(openai_client)
- return "uploaded:doc.txt"
- async def fake_wait(_file_id: str, timeout: int = 60, openai_client: object | None = None) -> None:
- del timeout
- seen.append(openai_client)
- return None
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler.upload_to_openai",
- fake_upload,
- )
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler._wait_for_file_processed",
- fake_wait,
- )
- result = await upload_from_urls(
- {"doc.txt": str(file_path)},
- allowed_local_dirs=[str(tmp_path)],
- openai_client=client_sentinel,
- )
- assert result == {"doc.txt": "uploaded:doc.txt"}
- assert seen == [client_sentinel, client_sentinel]
- @pytest.mark.asyncio
- async def test_upload_from_urls_rejects_directory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
- """Directories should not be accepted as local file attachments."""
- directory = tmp_path / "folder"
- directory.mkdir()
- async def fake_wait(_file_id: str) -> None:
- return None
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler._wait_for_file_processed",
- fake_wait,
- )
- with pytest.raises(IsADirectoryError, match="must be a file"):
- await upload_from_urls({"folder": str(directory)}, allowed_local_dirs=[str(tmp_path)])
- @pytest.mark.asyncio
- async def test_upload_from_urls_allowlist_enforcement(tmp_path: Path) -> None:
- """Disallowed or missing allowlist paths should block local uploads."""
- allowed_dir = tmp_path / "allowed"
- allowed_dir.mkdir()
- disallowed_dir = tmp_path / "other"
- disallowed_dir.mkdir()
- allowed_file = allowed_dir / "allowed.txt"
- allowed_file.write_text("ok", encoding="utf-8")
- disallowed_file = disallowed_dir / "doc.txt"
- disallowed_file.write_text("hello", encoding="utf-8")
- cases: list[tuple[str, list[str | Path] | None, str]] = [
- (str(disallowed_file), [str(allowed_dir)], "allowed directories"),
- (str(allowed_file), None, "Local file access is disabled"),
- (str(allowed_file), [tmp_path / "missing"], "Local file access is disabled"),
- ]
- for source, allowlist, error_match in cases:
- with pytest.raises(PermissionError, match=error_match):
- await upload_from_urls({"doc.txt": source}, allowed_local_dirs=allowlist)
- @pytest.mark.asyncio
- async def test_upload_from_urls_skips_missing_allowlist_when_valid_dir_exists(
- monkeypatch: pytest.MonkeyPatch,
- tmp_path: Path,
- ) -> None:
- """Missing allowlist entries should not block uploads from existing allowed dirs."""
- allowed_dir = tmp_path / "allowed"
- allowed_dir.mkdir()
- file_path = allowed_dir / "doc.txt"
- file_path.write_text("hello", encoding="utf-8")
- missing_dir = tmp_path / "missing"
- async def fake_upload(path: str) -> str:
- return f"uploaded:{Path(path).name}"
- async def fake_wait(_file_id: str) -> None:
- return None
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler.upload_to_openai",
- fake_upload,
- )
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler._wait_for_file_processed",
- fake_wait,
- )
- result = await upload_from_urls(
- {"doc.txt": str(file_path)},
- allowed_local_dirs=[str(allowed_dir), str(missing_dir)],
- )
- assert result == {"doc.txt": "uploaded:doc.txt"}
- @pytest.mark.asyncio
- async def test_upload_from_urls_rejects_non_directory_allowlist_even_with_valid_dir(tmp_path: Path) -> None:
- """Non-directory allowlist entries should fail fast instead of being silently ignored."""
- allowed_dir = tmp_path / "allowed"
- allowed_dir.mkdir()
- file_path = allowed_dir / "doc.txt"
- file_path.write_text("hello", encoding="utf-8")
- non_directory_entry = tmp_path / "not-a-dir.txt"
- non_directory_entry.write_text("x", encoding="utf-8")
- with pytest.raises(NotADirectoryError, match="Allowed path must be a directory"):
- await upload_from_urls(
- {"doc.txt": str(file_path)},
- allowed_local_dirs=[str(allowed_dir), str(non_directory_entry)],
- )
- @pytest.mark.asyncio
- async def test_upload_from_urls_expands_user_path_allowlist(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
- """Allowlist provided as Path('~') should expand and permit uploads in home."""
- home_dir = tmp_path / "home"
- home_dir.mkdir()
- file_path = home_dir / "doc.txt"
- file_path.write_text("hello", encoding="utf-8")
- monkeypatch.setenv("HOME", str(home_dir))
- monkeypatch.setenv("USERPROFILE", str(home_dir))
- async def fake_upload(path: str) -> str:
- return f"uploaded:{Path(path).name}"
- async def fake_wait(_file_id: str) -> None:
- return None
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler.upload_to_openai",
- fake_upload,
- )
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler._wait_for_file_processed",
- fake_wait,
- )
- result = await upload_from_urls({"doc.txt": str(file_path)}, allowed_local_dirs=[Path("~")])
- assert result == {"doc.txt": "uploaded:doc.txt"}
- @pytest.mark.asyncio
- async def test_upload_from_urls_remote_only_skips_allowlist_validation(
- monkeypatch: pytest.MonkeyPatch,
- tmp_path: Path,
- ) -> None:
- """Remote-only uploads should not fail when allowlist entries are missing."""
- async def fake_download(url: str, name: str, save_dir: str) -> str:
- dest = Path(save_dir) / name
- dest.write_text("remote data", encoding="utf-8")
- return str(dest)
- async def fake_upload(path: str) -> str:
- return f"uploaded:{Path(path).name}"
- async def fake_wait(_file_id: str) -> None:
- return None
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler.download_file",
- fake_download,
- )
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler.upload_to_openai",
- fake_upload,
- )
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler._wait_for_file_processed",
- fake_wait,
- )
- result = await upload_from_urls(
- {"doc.txt": "https://example.com/file.txt"},
- allowed_local_dirs=[str(tmp_path / "missing")],
- )
- assert result == {"doc.txt": "uploaded:doc.txt"}
- @pytest.mark.asyncio
- @pytest.mark.skipif(sys.platform != "win32", reason="UNC paths are Windows-specific")
- async def test_upload_from_urls_uploads_unc_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
- """UNC paths (//server/share) should be treated as local on Windows."""
- file_path = tmp_path / "doc.txt"
- file_path.write_text("hello", encoding="utf-8")
- async def fake_upload(path: str) -> str:
- return f"uploaded:{Path(path).name}"
- async def fake_wait(_file_id: str) -> None:
- return None
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler.upload_to_openai",
- fake_upload,
- )
- monkeypatch.setattr(
- "agency_swarm.integrations.fastapi_utils.file_handler._wait_for_file_processed",
- fake_wait,
- )
- unc_style = f"//{tmp_path.parts[0].rstrip(':')}/{'/'.join(tmp_path.parts[1:])}/doc.txt"
- with pytest.raises((PermissionError, FileNotFoundError)):
- await upload_from_urls({"doc.txt": unc_style}, allowed_local_dirs=[str(tmp_path)])
- @pytest.mark.asyncio
- async def test_upload_from_urls_rejects_nonexistent_local_file(tmp_path: Path) -> None:
- """Local paths to non-existent files should raise FileNotFoundError."""
- file_path = tmp_path / "nonexistent.txt"
- with pytest.raises(FileNotFoundError, match="Local file not found"):
- await upload_from_urls({"doc.txt": str(file_path)}, allowed_local_dirs=[str(tmp_path)])
|