"""Unit tests for session file API endpoint logic."""
import pytest
from fastapi import HTTPException
import navi.api.routes.sessions as sessions_mod
import navi.session_files as session_files_mod
from navi.auth import User
from navi.core.session import InMemorySessionStore
@pytest.fixture
async def store():
session_store = InMemorySessionStore()
await session_store.create("secretary")
return session_store
@pytest.fixture(autouse=True)
def _session_files_dir(monkeypatch, tmp_path):
from navi.config import Settings
async def _to_thread(func, *args, **kwargs):
return func(*args, **kwargs)
monkeypatch.setattr(sessions_mod.asyncio, "to_thread", _to_thread)
_test_settings = Settings(
session_files_dir=str(tmp_path / "sessions"),
session_files_max_size_mb=1,
)
monkeypatch.setattr(sessions_mod, "settings", _test_settings)
monkeypatch.setattr(session_files_mod, "settings", _test_settings)
async def _session_id(store: InMemorySessionStore) -> str:
sessions = await store.list_all()
return sessions[0].id
class FakeUploadFile:
def __init__(self, name: str, data: bytes, content_type: str = "application/octet-stream") -> None:
self.filename = name
self.content_type = content_type
self._data = data
self._offset = 0
async def read(self, size: int = -1) -> bytes:
if self._offset >= len(self._data):
return b""
if size < 0:
size = len(self._data) - self._offset
chunk = self._data[self._offset:self._offset + size]
self._offset += len(chunk)
return chunk
def _upload(name: str, data: bytes, content_type: str = "application/octet-stream") -> FakeUploadFile:
return FakeUploadFile(name, data, content_type)
_FAKE_USER = User(id="test-user", email="test@example.com", display_name="Test", role="admin", permissions=[])
async def test_upload_duplicate_filename_gets_numbered_name(store):
session_id = await _session_id(store)
first = await sessions_mod.upload_file(session_id, _upload("report.txt", b"one"), store, _FAKE_USER)
second = await sessions_mod.upload_file(session_id, _upload("report.txt", b"two"), store, _FAKE_USER)
assert first["name"] == "report.txt"
assert second["name"] == "report_1.txt"
async def test_upload_forbidden_extension_rejected(store):
session_id = await _session_id(store)
with pytest.raises(HTTPException) as exc:
await sessions_mod.upload_file(session_id, _upload("run.sh", b"echo nope"), store, _FAKE_USER)
assert exc.value.status_code == 400
assert "Executable files" in exc.value.detail
async def test_download_rejects_path_traversal(store):
session_id = await _session_id(store)
with pytest.raises(HTTPException) as exc:
await sessions_mod.download_file(session_id, "../secret.txt", store)
assert exc.value.status_code == 403
async def test_download_browser_renderable_file_is_inline(store):
session_id = await _session_id(store)
dest = session_files_mod.ensure_session_dir(session_id) / "page.html"
dest.write_text("<h1>Hello</h1>")
response = await sessions_mod.download_file(session_id, "page.html", store)
assert response.media_type == "text/html"
assert "inline" in response.headers["content-disposition"]
async def test_download_query_forces_attachment(store):
session_id = await _session_id(store)
dest = session_files_mod.ensure_session_dir(session_id) / "page.html"
dest.write_text("<h1>Hello</h1>")
response = await sessions_mod.download_file(session_id, "page.html", store, download=True)
assert response.media_type == "text/html"
assert "attachment" in response.headers["content-disposition"]
async def test_download_archive_is_attachment(store):
session_id = await _session_id(store)
dest = session_files_mod.ensure_session_dir(session_id) / "bundle.zip"
dest.write_bytes(b"zip")
response = await sessions_mod.download_file(session_id, "bundle.zip", store)
assert response.media_type == "application/zip"
assert "attachment" in response.headers["content-disposition"]
async def test_get_session_content_returns_store_items(store, monkeypatch):
session_id = await _session_id(store)
async def fake_list_for_session(sid):
assert sid == session_id
return [{"filename": "logo.svg"}]
monkeypatch.setattr(sessions_mod, "list_for_session", fake_list_for_session)
result = await sessions_mod.get_session_content(session_id, store)
assert result == {"session_id": session_id, "content": [{"filename": "logo.svg"}]}