generated from coulomb/repo-seed
Implement SAND-WP-0006: SaaS payments, routing, and ext.saas-stub
Add credits store, metering on create/destroy, extension routing resolver, metered SaaS stub extension, burst/saas profiles, credits CLI, docs, and tests.
This commit is contained in:
98
tests/test_payments.py
Normal file
98
tests/test_payments.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user