207 lines
7.7 KiB
Python
207 lines
7.7 KiB
Python
# #region Test.Api.ScenarioLiveBindings [C:3] [TYPE Module] [SEMANTICS test,api,scenario,execution,live-binding,admin]
|
|
# @BRIEF Admin CRUD for deployment live bindings via an isolated FastAPI app + fake ConfigManager.
|
|
# @RELATION BINDS_TO -> [Api.ScenarioLiveBindings]
|
|
# @RELATION BINDS_TO -> [ScenarioExecution.Runner.ConfiguredBinding]
|
|
# @TEST_CONTRACT: GET/PUT/PATCH /api/scenario-live-bindings -> validated settings.scenario_live_execution_bindings
|
|
# @TEST_EDGE: invalid_snapshot -> 422 before persistence
|
|
# @TEST_EDGE: binding_ref_mismatch -> 422
|
|
# @TEST_EDGE: unknown_toggle -> 404
|
|
# @TEST_INVARIANT Api.ScenarioLiveBindings: a registered, enabled binding is resolved server-side by
|
|
# ScenarioExecution.Runner.ConfiguredBinding; disabled/unregistered is not.
|
|
# -> VERIFIED_BY: test_registered_binding_resolves_server_side
|
|
# @TEST_INVARIANT Api.ScenarioLiveBindings: non-admin is denied 403 by the real has_permission checker.
|
|
# -> VERIFIED_BY: test_non_admin_denied_403
|
|
from __future__ import annotations
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.api.routes.dashboard_testing.scenario_live_bindings import router
|
|
from src.core.config_models import GlobalSettings
|
|
from src.dependencies import get_config_manager, get_current_user
|
|
from src.models.auth import Role, User
|
|
|
|
_BINDING_REF = "ss-prod-d11-live-001"
|
|
_SNAPSHOT = {
|
|
"binding_ref": _BINDING_REF,
|
|
"environment_id": "env-prod-01",
|
|
"dashboard_release_id": "release-1",
|
|
"release_fingerprint": "f" * 64,
|
|
"dashboard_id": 11,
|
|
"query_model_fingerprint": "q" * 64,
|
|
"execution_principal_fingerprint": "p" * 64,
|
|
"rls_security_fingerprint": "r" * 64,
|
|
"browser_safe_checkpoint_ref": None,
|
|
"browser_action_binding_ref": None,
|
|
"evidence_owner_type": "scenario_run",
|
|
"evidence_ref_policy": "draft_storage_raw_response",
|
|
}
|
|
|
|
|
|
_QUERY_MODEL = {
|
|
"schema_version": 1,
|
|
"environment_id": "env-prod-01",
|
|
"dashboard_id": 11,
|
|
"title": "Sales Dashboard",
|
|
"query_model_fingerprint": "q" * 64,
|
|
}
|
|
|
|
|
|
class _FakeConfig:
|
|
def __init__(self) -> None:
|
|
self.settings = GlobalSettings()
|
|
|
|
|
|
class _FakeConfigManager:
|
|
def __init__(self) -> None:
|
|
self._config = _FakeConfig()
|
|
|
|
def get_config(self):
|
|
return self._config
|
|
|
|
def update_global_settings(self, settings):
|
|
self._config.settings = settings
|
|
return self._config
|
|
|
|
|
|
def _admin() -> User:
|
|
role = Role(id="role-admin", name="admin", is_admin=True)
|
|
role.permissions = []
|
|
user = User(id="user-admin", username="admin", email="admin@test.com")
|
|
user.roles = [role]
|
|
return user
|
|
|
|
|
|
def _outsider() -> User:
|
|
role = Role(id="role-viewer", name="viewer", is_admin=False)
|
|
role.permissions = []
|
|
user = User(id="user-viewer", username="viewer", email="viewer@test.com")
|
|
user.roles = [role]
|
|
return user
|
|
|
|
|
|
class _Env:
|
|
def __init__(self) -> None:
|
|
self.config_manager = _FakeConfigManager()
|
|
self.app = FastAPI()
|
|
self.app.include_router(router)
|
|
self.app.dependency_overrides[get_config_manager] = lambda: self.config_manager
|
|
self.client = TestClient(self.app)
|
|
|
|
def as_user(self, user: User) -> None:
|
|
self.app.dependency_overrides[get_current_user] = lambda: user
|
|
|
|
|
|
def _put_body(**overrides) -> dict:
|
|
body = {"enabled": True, "binding_snapshot": dict(_SNAPSHOT), "query_model_snapshot": dict(_QUERY_MODEL)}
|
|
body.update(overrides)
|
|
return body
|
|
|
|
|
|
# #region Test.Api.ScenarioLiveBindings.Crud [C:2] [TYPE Function]
|
|
def test_list_empty_then_register_and_toggle():
|
|
env = _Env()
|
|
env.as_user(_admin())
|
|
|
|
assert env.client.get("/api/scenario-live-bindings").json() == []
|
|
|
|
created = env.client.put(f"/api/scenario-live-bindings/{_BINDING_REF}", json=_put_body())
|
|
assert created.status_code == 200
|
|
assert created.json()["binding_ref"] == _BINDING_REF
|
|
assert created.json()["enabled"] is True
|
|
|
|
listed = env.client.get("/api/scenario-live-bindings").json()
|
|
assert [item["binding_ref"] for item in listed] == [_BINDING_REF]
|
|
|
|
toggled = env.client.patch(f"/api/scenario-live-bindings/{_BINDING_REF}", json={"enabled": False})
|
|
assert toggled.status_code == 200
|
|
assert toggled.json()["enabled"] is False
|
|
|
|
|
|
def test_put_rejects_invalid_snapshot_without_persisting():
|
|
env = _Env()
|
|
env.as_user(_admin())
|
|
bad = {**_SNAPSHOT, "evidence_owner_type": "draft"}
|
|
response = env.client.put(f"/api/scenario-live-bindings/{_BINDING_REF}", json=_put_body(binding_snapshot=bad))
|
|
assert response.status_code == 422
|
|
assert env.config_manager.get_config().settings.scenario_live_execution_bindings == []
|
|
|
|
|
|
def test_put_rejects_binding_ref_mismatch():
|
|
env = _Env()
|
|
env.as_user(_admin())
|
|
response = env.client.put("/api/scenario-live-bindings/other-ref", json=_put_body())
|
|
assert response.status_code == 422
|
|
assert env.config_manager.get_config().settings.scenario_live_execution_bindings == []
|
|
|
|
|
|
def test_put_rejects_invalid_and_mismatched_query_model():
|
|
env = _Env()
|
|
env.as_user(_admin())
|
|
invalid = env.client.put(
|
|
f"/api/scenario-live-bindings/{_BINDING_REF}",
|
|
json=_put_body(query_model_snapshot={"totally": "bogus"}),
|
|
)
|
|
assert invalid.status_code == 422
|
|
|
|
mismatched = env.client.put(
|
|
f"/api/scenario-live-bindings/{_BINDING_REF}",
|
|
json=_put_body(query_model_snapshot={**_QUERY_MODEL, "environment_id": "other-env"}),
|
|
)
|
|
assert mismatched.status_code == 422
|
|
|
|
fingerprint_mismatch = env.client.put(
|
|
f"/api/scenario-live-bindings/{_BINDING_REF}",
|
|
json=_put_body(query_model_snapshot={**_QUERY_MODEL, "query_model_fingerprint": "z" * 64}),
|
|
)
|
|
assert fingerprint_mismatch.status_code == 422
|
|
assert env.config_manager.get_config().settings.scenario_live_execution_bindings == []
|
|
|
|
|
|
def test_toggle_unknown_binding_is_404():
|
|
env = _Env()
|
|
env.as_user(_admin())
|
|
response = env.client.patch("/api/scenario-live-bindings/missing-ref", json={"enabled": True})
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_non_admin_denied_403():
|
|
env = _Env()
|
|
env.as_user(_outsider())
|
|
assert env.client.get("/api/scenario-live-bindings").status_code == 403
|
|
assert env.client.put(f"/api/scenario-live-bindings/{_BINDING_REF}", json=_put_body()).status_code == 403
|
|
assert env.client.patch(f"/api/scenario-live-bindings/{_BINDING_REF}", json={"enabled": True}).status_code == 403
|
|
# #endregion Test.Api.ScenarioLiveBindings.Crud
|
|
|
|
|
|
# #region Test.Api.ScenarioLiveBindings.Resolve [C:2] [TYPE Function]
|
|
def test_registered_binding_resolves_server_side():
|
|
from src.services.dashboard_testing.execution.start_run import _resolve_configured_live_binding
|
|
|
|
env = _Env()
|
|
env.as_user(_admin())
|
|
env.client.put(f"/api/scenario-live-bindings/{_BINDING_REF}", json=_put_body())
|
|
plan = {"steps": [{"dashboard_id": 11}]}
|
|
principal = _SNAPSHOT["execution_principal_fingerprint"]
|
|
|
|
resolved = _resolve_configured_live_binding(
|
|
environment_id="env-prod-01", plan=plan, principal_fingerprint=principal,
|
|
config_manager=env.config_manager,
|
|
)
|
|
assert resolved is not None
|
|
assert resolved.binding_ref == _BINDING_REF
|
|
|
|
# A different principal or environment never adopts the binding.
|
|
assert _resolve_configured_live_binding(
|
|
environment_id="env-prod-01", plan=plan, principal_fingerprint="z" * 64,
|
|
config_manager=env.config_manager,
|
|
) is None
|
|
|
|
env.client.patch(f"/api/scenario-live-bindings/{_BINDING_REF}", json={"enabled": False})
|
|
assert _resolve_configured_live_binding(
|
|
environment_id="env-prod-01", plan=plan, principal_fingerprint=principal,
|
|
config_manager=env.config_manager,
|
|
) is None
|
|
# #endregion Test.Api.ScenarioLiveBindings.Resolve
|
|
# #endregion Test.Api.ScenarioLiveBindings
|