Files
2026-07-13 13:12:33 +08:00

319 lines
11 KiB
Python

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"<!doctype html><title>ok</title>",
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"<!doctype html><title>ok</title>"
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"<html></html>", 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"<html></html>", 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"<html></html>", 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