from __future__ import annotations from pathlib import Path from types import SimpleNamespace import pytest from opensquilla.artifacts import ArtifactStore class _FakeSessionManager: def __init__(self, session_id: str) -> None: self.session_id = session_id async def get_session(self, session_key: str) -> object | None: if session_key == "agent:main:webchat:ok": return SimpleNamespace(session_id=self.session_id) return None def _app(tmp_path: Path, *, auth_mode: str = "token", host: str = "127.0.0.1"): pytest.importorskip("starlette.testclient") from starlette.applications import Starlette from opensquilla.gateway.artifacts import register_artifact_routes from opensquilla.gateway.config import AttachmentsConfig, AuthConfig, GatewayConfig from opensquilla.gateway.middleware import AuthMiddleware config = GatewayConfig( host=host, auth=AuthConfig(mode=auth_mode, token="secret"), attachments=AttachmentsConfig(media_root=str(tmp_path)), ) app = Starlette(debug=False) register_artifact_routes( app, config=config, session_manager=_FakeSessionManager("session-1"), ) app.add_middleware(AuthMiddleware, config=config) return app def _publish( tmp_path: Path, *, payload: bytes = b"hello artifact", name: str = "report final.txt", mime: str = "text/plain", ): return ArtifactStore(tmp_path).publish_bytes( payload, session_id="session-1", session_key="agent:main:webchat:ok", name=name, mime=mime, source="publish_artifact", ) def test_artifact_download_requires_auth_and_session_scope(tmp_path: Path) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient ref = _publish(tmp_path) with TestClient(_app(tmp_path)) as client: unauthenticated = client.get( f"/api/v1/artifacts/{ref.id}?sessionKey=agent:main:webchat:ok" ) wrong_session = client.get( f"/api/v1/artifacts/{ref.id}?sessionKey=agent:main:webchat:other", headers={"Authorization": "Bearer secret"}, ) missing_session = client.get( f"/api/v1/artifacts/{ref.id}", headers={"Authorization": "Bearer secret"}, ) assert unauthenticated.status_code == 401 assert wrong_session.status_code == 404 assert missing_session.status_code == 404 def test_artifact_download_serves_file_response_headers_and_ranges(tmp_path: Path) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient ref = _publish(tmp_path) with TestClient(_app(tmp_path)) as client: response = client.get( f"/api/v1/artifacts/{ref.id}?sessionKey=agent:main:webchat:ok", headers={"Authorization": "Bearer secret"}, ) ranged = client.get( f"/api/v1/artifacts/{ref.id}?sessionKey=agent:main:webchat:ok", headers={"Authorization": "Bearer secret", "Range": "bytes=0-4"}, ) assert response.status_code == 200 assert response.content == b"hello artifact" assert response.headers["content-type"].startswith("text/plain") assert "attachment" in response.headers["content-disposition"] assert "report%20final.txt" in response.headers["content-disposition"] assert ranged.status_code == 206 assert ranged.content == b"hello" def test_artifact_download_reports_not_found_and_integrity_errors(tmp_path: Path) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient ref = _publish(tmp_path) ArtifactStore(tmp_path).path_for(ref).write_bytes(b"tampered") with TestClient(_app(tmp_path)) as client: missing = client.get( "/api/v1/artifacts/missing?sessionKey=agent:main:webchat:ok", headers={"Authorization": "Bearer secret"}, ) mismatch = client.get( f"/api/v1/artifacts/{ref.id}?sessionKey=agent:main:webchat:ok", headers={"Authorization": "Bearer secret"}, ) assert missing.status_code == 404 assert mismatch.status_code == 409 def test_artifact_native_open_owner_opens_html_copy(tmp_path: Path, monkeypatch) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient from opensquilla.gateway import artifacts as artifact_routes ref = _publish( tmp_path, payload=b"ok", name="report.html", mime="text/html", ) opened: list[Path] = [] monkeypatch.setattr(artifact_routes.tempfile, "gettempdir", lambda: str(tmp_path / "tmp")) monkeypatch.setattr( artifact_routes, "_open_path_with_default_app", lambda path: opened.append(Path(path)) or None, ) with TestClient(_app(tmp_path), client=("127.0.0.1", 50000)) as client: response = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={ "Authorization": "Bearer secret", "x-opensquilla-session-key": "agent:main:webchat:ok", }, ) assert response.status_code == 202 assert response.json() == {"ok": True, "status": "accepted"} assert len(opened) == 1 assert opened[0].name.endswith("-report.html") assert opened[0].read_bytes() == b"ok" def test_artifact_open_cache_dir_skips_posix_mode_bits_on_windows( tmp_path: Path, monkeypatch, ) -> None: from opensquilla.gateway import artifacts as artifact_routes temp_root = tmp_path / "tmp" cache_root = temp_root / "opensquilla-artifacts" cache_root.mkdir(parents=True) cache_root.chmod(0o777) monkeypatch.setattr(artifact_routes.tempfile, "gettempdir", lambda: str(temp_root)) monkeypatch.setattr(artifact_routes.sys, "platform", "win32") assert artifact_routes._artifact_open_cache_dir() == cache_root def test_artifact_native_open_requires_auth_and_owner(tmp_path: Path, monkeypatch) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient from opensquilla.gateway import artifacts as artifact_routes ref = _publish(tmp_path, payload=b"", name="page.html", mime="text/html") opened: list[Path] = [] monkeypatch.setattr( artifact_routes, "_open_path_with_default_app", lambda path: opened.append(Path(path)) or None, ) with TestClient(_app(tmp_path), client=("127.0.0.1", 50000)) as client: unauthenticated = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={"x-opensquilla-session-key": "agent:main:webchat:ok"}, ) with TestClient( _app(tmp_path, auth_mode="none", host="0.0.0.0"), client=("127.0.0.1", 50000), ) as client: non_owner = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={"x-opensquilla-session-key": "agent:main:webchat:ok"}, ) assert unauthenticated.status_code == 401 assert non_owner.status_code == 403 assert non_owner.json()["code"] == "OWNER_REQUIRED" assert opened == [] def test_artifact_native_open_requires_session_scope_and_integrity( tmp_path: Path, monkeypatch, ) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient from opensquilla.gateway import artifacts as artifact_routes ref = _publish(tmp_path, payload=b"", name="page.html", mime="text/html") monkeypatch.setattr(artifact_routes.tempfile, "gettempdir", lambda: str(tmp_path / "tmp")) monkeypatch.setattr(artifact_routes, "_open_path_with_default_app", lambda _path: None) with TestClient(_app(tmp_path), client=("127.0.0.1", 50000)) as client: wrong_session = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={ "Authorization": "Bearer secret", "x-opensquilla-session-key": "agent:main:webchat:other", }, ) missing_session = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={"Authorization": "Bearer secret"}, ) ArtifactStore(tmp_path).path_for(ref).write_bytes(b"tampered") with TestClient(_app(tmp_path), client=("127.0.0.1", 50000)) as client: mismatch = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={ "Authorization": "Bearer secret", "x-opensquilla-session-key": "agent:main:webchat:ok", }, ) assert wrong_session.status_code == 404 assert missing_session.status_code == 404 assert mismatch.status_code == 409 assert mismatch.json()["code"] == "INTEGRITY_ERROR" def test_artifact_native_open_rejects_non_html_artifacts(tmp_path: Path, monkeypatch) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient from opensquilla.gateway import artifacts as artifact_routes ref = _publish(tmp_path, payload=b"plain", name="notes.txt", mime="text/plain") opened: list[Path] = [] monkeypatch.setattr( artifact_routes, "_open_path_with_default_app", lambda path: opened.append(Path(path)) or None, ) with TestClient(_app(tmp_path), client=("127.0.0.1", 50000)) as client: response = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={ "Authorization": "Bearer secret", "x-opensquilla-session-key": "agent:main:webchat:ok", }, ) assert response.status_code == 415 assert response.json()["code"] == "UNSUPPORTED_ARTIFACT_OPEN" assert opened == [] def test_artifact_native_open_opener_failure_does_not_leak_path( tmp_path: Path, monkeypatch, ) -> None: pytest.importorskip("starlette.testclient") from starlette.testclient import TestClient from opensquilla.gateway import artifacts as artifact_routes ref = _publish(tmp_path, payload=b"", name="page.html", mime="text/html") opened: list[Path] = [] def fail_open(path: Path) -> str: opened.append(Path(path)) return f"failed to open {path}" monkeypatch.setattr(artifact_routes.tempfile, "gettempdir", lambda: str(tmp_path / "tmp")) monkeypatch.setattr(artifact_routes, "_open_path_with_default_app", fail_open) with TestClient(_app(tmp_path), client=("127.0.0.1", 50000)) as client: response = client.post( f"/api/v1/artifacts/{ref.id}/open", headers={ "Authorization": "Bearer secret", "x-opensquilla-session-key": "agent:main:webchat:ok", }, ) assert response.status_code == 503 assert response.json()["code"] == "OPEN_FAILED" assert opened assert str(opened[0]) not in response.text