generated from coulomb/repo-seed
Add credits store, metering on create/destroy, extension routing resolver, metered SaaS stub extension, burst/saas profiles, credits CLI, docs, and tests.
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
"""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) |