Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a06def-6490-7033-8448-2eab2d12ed44
213 lines
6.6 KiB
Python
213 lines
6.6 KiB
Python
"""HTTP API v0 stub tests."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from sandboxer.api.app import app
|
|
from sandboxer.models import (
|
|
ActorType,
|
|
Consumer,
|
|
SandboxExecResult,
|
|
SandboxState,
|
|
SandboxStatus,
|
|
SnapshotRecord,
|
|
)
|
|
|
|
|
|
def test_list_sandboxes_empty() -> None:
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.list.return_value = []
|
|
client = TestClient(app)
|
|
assert client.get("/v1/sandboxes").json() == []
|
|
|
|
|
|
def test_get_sandbox_not_found() -> None:
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.get.return_value = None
|
|
client = TestClient(app)
|
|
assert client.get("/v1/sandboxes/missing").status_code == 404
|
|
|
|
|
|
def test_create_sandbox() -> None:
|
|
from datetime import UTC, datetime
|
|
|
|
status = SandboxStatus(
|
|
sandbox_id="abc12345",
|
|
profile_id="profile.compose-e2e",
|
|
extension_id="ext.compose-ssh",
|
|
state=SandboxState.READY,
|
|
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.create.return_value = status
|
|
client = TestClient(app)
|
|
resp = client.post(
|
|
"/v1/sandboxes",
|
|
json={
|
|
"profile": "profile.compose-e2e",
|
|
"inputs": {"repo": "/tmp/x"},
|
|
"consumer": {"actor": "adm", "project": "sand-boxer"},
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["sandbox_id"] == "abc12345"
|
|
|
|
|
|
def test_snapshot_sandbox() -> None:
|
|
from datetime import UTC, datetime
|
|
|
|
record = SnapshotRecord(
|
|
snapshot_id="snap12345678",
|
|
sandbox_id="abc12345",
|
|
profile_id="profile.compose-checkpoint",
|
|
extension_id="ext.compose-ssh",
|
|
host="coulombcore",
|
|
created_at=datetime.now(UTC),
|
|
)
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.snapshot.return_value = record
|
|
client = TestClient(app)
|
|
resp = client.post("/v1/sandboxes/abc12345/snapshot")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["snapshot_id"] == "snap12345678"
|
|
|
|
|
|
def test_restore_snapshot() -> None:
|
|
from datetime import UTC, datetime
|
|
|
|
status = SandboxStatus(
|
|
sandbox_id="restored1",
|
|
profile_id="profile.compose-checkpoint",
|
|
extension_id="ext.compose-ssh",
|
|
state=SandboxState.READY,
|
|
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.restore.return_value = status
|
|
client = TestClient(app)
|
|
resp = client.post(
|
|
"/v1/snapshots/snap12345678/restore",
|
|
json={"consumer": {"actor": "adm", "project": "sand-boxer"}},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["sandbox_id"] == "restored1"
|
|
|
|
|
|
def test_recreate_sandbox() -> None:
|
|
from datetime import UTC, datetime
|
|
|
|
status = SandboxStatus(
|
|
sandbox_id="new12345",
|
|
profile_id="profile.compose-e2e",
|
|
extension_id="ext.compose-ssh",
|
|
state=SandboxState.READY,
|
|
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.recreate.return_value = status
|
|
client = TestClient(app)
|
|
resp = client.post("/v1/sandboxes/abc12345/recreate")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["sandbox_id"] == "new12345"
|
|
|
|
|
|
def test_extend_ttl() -> None:
|
|
from datetime import UTC, datetime
|
|
|
|
now = datetime.now(UTC)
|
|
status = SandboxStatus(
|
|
sandbox_id="abc12345",
|
|
profile_id="profile.compose-e2e",
|
|
extension_id="ext.compose-ssh",
|
|
state=SandboxState.READY,
|
|
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
|
ttl="2h",
|
|
expires_at=now,
|
|
created_at=now,
|
|
updated_at=now,
|
|
ready_at=now,
|
|
)
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.extend_ttl.return_value = status
|
|
client = TestClient(app)
|
|
resp = client.patch(
|
|
"/v1/sandboxes/abc12345/ttl",
|
|
json={"duration": "2h"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["ttl"] == "2h"
|
|
|
|
|
|
def test_expire_sandboxes() -> None:
|
|
from sandboxer.models import ExpireActionResult
|
|
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.expire.return_value = [
|
|
ExpireActionResult(sandbox_id="x", reason="ttl", action="dry-run")
|
|
]
|
|
client = TestClient(app)
|
|
resp = client.post("/v1/sandboxes/expire")
|
|
assert resp.status_code == 200
|
|
assert resp.json()[0]["action"] == "dry-run"
|
|
|
|
|
|
def test_exec_api_fails_closed_when_owner_token_unconfigured(monkeypatch) -> None:
|
|
monkeypatch.delenv("SANDBOXER_EXEC_TOKEN", raising=False)
|
|
client = TestClient(app)
|
|
resp = client.post(
|
|
"/v1/sandboxes/abc12345/exec",
|
|
json={
|
|
"command": ["true"],
|
|
"consumer": {"actor": "agt", "project": "glas-harness", "run_id": "run-1"},
|
|
},
|
|
)
|
|
assert resp.status_code == 503
|
|
|
|
|
|
def test_exec_api_requires_bearer_and_returns_execution(monkeypatch) -> None:
|
|
from datetime import UTC, datetime
|
|
|
|
monkeypatch.setenv("SANDBOXER_EXEC_TOKEN", "owner-capability")
|
|
now = datetime.now(UTC)
|
|
consumer = Consumer(actor="agt", project="glas-harness", run_id="run-1")
|
|
result = SandboxExecResult(
|
|
sandbox_id="abc12345",
|
|
profile_id="profile.bwrap-local",
|
|
extension_id="ext.bwrap",
|
|
consumer=consumer,
|
|
command_name="true",
|
|
exit_code=0,
|
|
duration_seconds=0.1,
|
|
workspace_dir="/tmp/sandboxer-bwrap/abc12345",
|
|
network_default="deny",
|
|
started_at=now,
|
|
completed_at=now,
|
|
)
|
|
payload = {
|
|
"command": ["true"],
|
|
"consumer": consumer.model_dump(mode="json"),
|
|
}
|
|
with patch("sandboxer.api.app._manager") as mgr:
|
|
mgr.execute.return_value = result
|
|
client = TestClient(app)
|
|
denied = client.post(
|
|
"/v1/sandboxes/abc12345/exec",
|
|
json=payload,
|
|
headers={"Authorization": "Bearer wrong"},
|
|
)
|
|
allowed = client.post(
|
|
"/v1/sandboxes/abc12345/exec",
|
|
json=payload,
|
|
headers={"Authorization": "Bearer owner-capability"},
|
|
)
|
|
|
|
assert denied.status_code == 401
|
|
assert allowed.status_code == 200
|
|
assert allowed.json()["exit_code"] == 0
|