import os import socket import subprocess from pathlib import Path from types import SimpleNamespace from urllib.parse import quote import psutil import pytest from archivebox.tests.conftest import ADMIN_TEST_HOST, run_archivebox_cmd pytestmark = pytest.mark.django_db(transaction=True) def _free_port() -> int: with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) return int(sock.getsockname()[1]) def _reset_runtime_config() -> None: from archivebox.config import common from archivebox.config.configset import _INI_CACHE from archivebox.machine.models import Machine _INI_CACHE.clear() for value in vars(common).values(): cache_clear = getattr(value, "cache_clear", None) if cache_clear is not None: cache_clear() Machine.current(refresh=True) def _set_archivebox_config(data_dir: Path, *values: str, env: dict[str, str] | None = None) -> None: os.chdir(data_dir) result = run_archivebox_cmd( ["config", "--set", *values], cwd=data_dir, env=env, timeout=120, ) assert result.returncode == 0, result.stderr or result.stdout _reset_runtime_config() @pytest.fixture def opencode_archive_config(initialized_archive): port = _free_port() state_dir = initialized_archive / "opencode" env = os.environ.copy() env.update( { "ABXPKG_INSTALL_TIMEOUT": "900", "ABXPKG_MIN_RELEASE_AGE": "0", "ABX_RUNTIME": "archivebox", "ARCHIVEBOX_ALLOW_NO_UNIX_SOCKETS": "true", "OPENCODE_ENABLED": "True", "OPENCODE_HOST": "127.0.0.1", "OPENCODE_PORT": str(port), "OPENCODE_WORKDIR": str(initialized_archive), "OPENCODE_STATE_DIR": str(state_dir), "OPENCODE_TIMEOUT": "60", }, ) _set_archivebox_config( initialized_archive, "OPENCODE_ENABLED=True", "OPENCODE_HOST=127.0.0.1", f"OPENCODE_PORT={port}", f"OPENCODE_WORKDIR={initialized_archive}", f"OPENCODE_STATE_DIR={state_dir}", "OPENCODE_TIMEOUT=60", env=env, ) return SimpleNamespace(data_dir=initialized_archive, port=port, state_dir=state_dir, env=env) @pytest.fixture def live_opencode(opencode_archive_config): from abx_plugins.plugins.opencode import views install = run_archivebox_cmd( ["install", "opencode", "--binproviders=env,pnpm"], cwd=opencode_archive_config.data_dir, env=opencode_archive_config.env, timeout=1200, ) assert install.returncode == 0, install.stderr or install.stdout _reset_runtime_config() config = views._machine_config() settings = views._settings(config) settings["archivebox_base_url"] = "http://admin.archivebox.localhost:8000" settings["archivebox_admin_url"] = "http://admin.archivebox.localhost:8000/admin" settings["archivebox_api_url"] = "http://admin.archivebox.localhost:8000/api/" binary, binary_env = views._resolve_binary(settings["binary"], settings["config"]) version = subprocess.run( [binary, "--version"], env={**os.environ, **binary_env}, text=True, capture_output=True, timeout=120, ) assert version.returncode == 0, version.stderr or version.stdout ok, error = views._ensure_opencode(settings) assert ok, error process = views._PROCESS assert process is not None try: yield SimpleNamespace(config=opencode_archive_config, settings=settings, process=process) finally: if views._PROCESS and views._PROCESS.poll() is None: views._PROCESS.terminate() try: views._PROCESS.wait(timeout=10) except Exception: views._PROCESS.kill() views._PROCESS = None def test_opencode_disabled_route_does_not_start_server(client, initialized_archive): from archivebox.machine.models import Machine from abx_plugins.plugins.opencode import views os.chdir(initialized_archive) Machine.from_json({"config": {"OPENCODE_ENABLED": False}}) _reset_runtime_config() assert views._machine_config()["OPENCODE_ENABLED"] is False response = client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST) assert response.status_code == 404 assert views._PROCESS is None or views._PROCESS.poll() is not None def test_opencode_agent_requires_superuser_when_enabled(client, db, django_user_model, live_opencode): response = client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST) assert response.status_code == 302 assert "/admin/login/" in response.headers["Location"] user = django_user_model.objects.create_user(username="regular", password="testpassword") client.force_login(user) response = client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST) assert response.status_code == 403 def test_opencode_proxy_blocks_cross_origin_mutation(admin_client, db, live_opencode): response = admin_client.post( "/admin/agent/opencode/session", data=b"{}", content_type="application/json", HTTP_HOST=ADMIN_TEST_HOST, HTTP_ORIGIN="https://evil.example", ) assert response.status_code == 403 def test_opencode_proxy_blocks_cross_site_fetch_metadata(admin_client, db, live_opencode): response = admin_client.post( "/admin/agent/opencode/session", data=b"{}", content_type="application/json", HTTP_HOST=ADMIN_TEST_HOST, HTTP_SEC_FETCH_SITE="cross-site", ) assert response.status_code == 403 def test_opencode_agent_superuser_gets_admin_wrapper(admin_client, live_opencode): from abx_plugins.plugins.opencode import views response = admin_client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST) assert response.status_code == 200 assert f'