"""Payments and metering integration tests.""" from __future__ import annotations from pathlib import Path from unittest.mock import patch from sandboxer.core.manager import SandboxManager from sandboxer.lifecycle.store import SandboxStore from sandboxer.models import ActorType, Consumer, SandboxCreateRequest, SandboxState from sandboxer.payments.credits import CreditsStore class SaaSBackend: def provision(self, profile, inputs, host): return {"sandbox_id": "saas1234", "host": "saas-stub", "endpoint": "https://x"} def wait_ready(self, handle): return {"endpoint": handle["endpoint"], "host": handle["host"]} def teardown(self, handle): return {"provider_removed": "true"} def estimate_cost(self, profile, inputs, *, duration_s=3600): from sandboxer.models import MeterQuote return MeterQuote( extension_id="ext.saas-stub", estimated_usd=0.5, duration_s=duration_s, ) def meter_actual(self, handle, *, duration_s: float) -> float: return 0.25 def _set_balance(credits: CreditsStore, amount: float) -> None: credits._write({"balance_usd": amount, "currency": "USD"}) def test_saas_create_destroy_debits_credits(tmp_path: Path) -> None: store = SandboxStore(path=tmp_path / "sandboxes.json") credits = CreditsStore(path=tmp_path / "credits.json") _set_balance(credits, 5.0) manager = SandboxManager(store=store, credits=credits) request = SandboxCreateRequest( profile="profile.saas-stub", inputs={}, consumer=Consumer(actor=ActorType.ADM, project="test"), ) backend = SaaSBackend() with ( patch("sandboxer.core.manager.resolve_extension") as resolve_ext, patch("sandboxer.core.manager.resolve_backend", return_value=backend), patch("sandboxer.payments.metering.resolve_backend", return_value=backend), patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None), ): from sandboxer.extensions.registry import load_extension resolve_ext.return_value = load_extension("ext.saas-stub") status = manager.create(request) assert status.state == SandboxState.READY assert status.meter and status.meter.estimate_usd == 0.5 destroyed = manager.destroy(status.sandbox_id) assert destroyed.state == SandboxState.DESTROYED assert destroyed.meter and destroyed.meter.actual_usd == 0.25 assert credits.balance() == 4.75 def test_insufficient_credits_blocks_create(tmp_path: Path) -> None: store = SandboxStore(path=tmp_path / "sandboxes.json") credits = CreditsStore(path=tmp_path / "credits.json") _set_balance(credits, 0.01) manager = SandboxManager(store=store, credits=credits) request = SandboxCreateRequest( profile="profile.saas-stub", inputs={}, consumer=Consumer(actor=ActorType.ADM, project="test"), ) with ( patch("sandboxer.core.manager.resolve_extension") as resolve_ext, patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None), ): from sandboxer.extensions.registry import load_extension resolve_ext.return_value = load_extension("ext.saas-stub") with patch("sandboxer.core.manager.resolve_backend", return_value=SaaSBackend()): try: manager.create(request) raise AssertionError("expected RuntimeError") except RuntimeError as exc: assert "Insufficient credits" in str(exc)