"""Live e2e: the config and miscellaneous Management/UI routes. One method per registry cell, each asserting the real contract against a live proxy: read-only inventory routes return their documented shape, stateless validators compute their verdict from the request, and the write routes persist so a read-back reflects the change. Router settings, which mutate global proxy state, are exercised with a benign, self-restoring change so a shared proxy is left as it was found. Cache settings and the Vault config override are deliberately not covered here. Both routes reconfigure the whole proxy: /cache/settings persists what it receives into a row that outranks the YAML cache_params and is re-applied on a timer, and /config_overrides/hashicorp_vault swaps the process-wide secret manager. Neither can be exercised safely against the shared proxy the suites run on, so they need an isolated proxy before a test lands. Do not add a read-then-write-back test for either one. """ from __future__ import annotations import math import time from collections.abc import Callable import pytest from pydantic import BaseModel from e2e_config import unique_marker from e2e_http import NoBody, Success, unwrap, unwrap_status from lifecycle import ResourceManager from management_client import ManagementClient from models import KeyGenerateBody, LiteLLMParamsBody, TeamNewBody pytestmark = pytest.mark.e2e def _poll[T](client: ManagementClient, attempt: Callable[[], T | None], failure: str) -> T: deadline = time.monotonic() + client.proxy.poll_timeout while time.monotonic() < deadline: found = attempt() if found is not None: return found time.sleep(client.proxy.poll_interval) pytest.fail(failure) # ---- callbacks ------------------------------------------------------------- class CallbacksListResponse(BaseModel): success: list[str] failure: list[str] success_and_failure: list[str] # ---- cost estimate --------------------------------------------------------- class CostEstimateBody(BaseModel): model: str input_tokens: int output_tokens: int num_requests_per_day: int | None = None class CostEstimateResponse(BaseModel): model: str input_tokens: int output_tokens: int cost_per_request: float input_cost_per_request: float output_cost_per_request: float margin_cost_per_request: float daily_cost: float | None = None provider: str | None = None # ---- credential migration check -------------------------------------------- class MigrationReport(BaseModel): residual_legacy: int total_undecryptable: int class MigrationCheckResponse(BaseModel): status: str report: MigrationReport # ---- tool + workflow inventories ------------------------------------------- class ToolListEntry(BaseModel): name: str | None = None class ToolListResponse(BaseModel): tools: list[ToolListEntry] total: int class WorkflowRunEntry(BaseModel): workflow_id: str | None = None class WorkflowRunsResponse(BaseModel): runs: list[WorkflowRunEntry] count: int # ---- compliance ------------------------------------------------------------ class ComplianceGdprBody(BaseModel): request_id: str user_id: str model: str timestamp: str class ComplianceCheck(BaseModel): check_name: str article: str passed: bool detail: str class ComplianceResponse(BaseModel): compliant: bool regulation: str checks: list[ComplianceCheck] # ---- fallback management --------------------------------------------------- class FallbackShape(BaseModel): model: str fallback_models: list[str] fallback_type: str class FallbackCreateBody(FallbackShape): pass class FallbackResponse(FallbackShape): message: str class FallbackGetParams(BaseModel): fallback_type: str class FallbackGetResponse(FallbackShape): pass # ---- jwt key mapping ------------------------------------------------------- class JwtKeyMappingNewBody(BaseModel): jwt_claim_name: str jwt_claim_value: str key: str description: str class JwtInfoParams(BaseModel): id: str class JwtDeleteBody(BaseModel): id: str class JwtKeyMappingResponse(BaseModel): id: str jwt_claim_name: str jwt_claim_value: str is_active: bool description: str | None = None # ---- router settings via /config/update ------------------------------------ class RouterSettingsPatch(BaseModel): num_retries: int class ConfigUpdateBody(BaseModel): router_settings: RouterSettingsPatch class ConfigUpdateResponse(BaseModel): message: str class RouterCurrentValues(BaseModel): num_retries: int | None = None class RouterSettingsResponse(BaseModel): current_values: RouterCurrentValues # ---- mcp server submission ------------------------------------------------- class McpRegisterBody(BaseModel): server_name: str url: str transport: str description: str class McpServerResponse(BaseModel): server_id: str server_name: str | None = None approval_status: str transport: str url: str | None = None class TestInventoryRoutes: @pytest.mark.covers("mgmt.callback.list.happy_path") def test_callbacks_list_reports_active_logging_callbacks(self, client: ManagementClient) -> None: listing = unwrap( client.proxy.transport.get( "/callbacks/list", headers=client.proxy.transport.master, params=NoBody(), response_type=CallbacksListResponse, ) ) every = [*listing.success, *listing.failure, *listing.success_and_failure] assert every, "/callbacks/list reported no active logging callbacks; the proxy always runs the db logger" assert "_ProxyDBLogger" in every, ( f"/callbacks/list omitted the always-on _ProxyDBLogger spend logger; got {every}" ) @pytest.mark.covers("mgmt.tool_management.list.happy_path") def test_tool_list_returns_catalog_with_consistent_total(self, client: ManagementClient) -> None: listing = unwrap( client.proxy.transport.get( "/v1/tool/list", headers=client.proxy.transport.master, params=NoBody(), response_type=ToolListResponse, ) ) assert listing.total == len(listing.tools), ( f"/v1/tool/list total {listing.total} disagrees with the {len(listing.tools)} tools returned" ) @pytest.mark.covers("mgmt.workflow.list.happy_path") def test_workflow_runs_list_returns_consistent_count(self, client: ManagementClient) -> None: listing = unwrap( client.proxy.transport.get( "/v1/workflows/runs", headers=client.proxy.transport.master, params=NoBody(), response_type=WorkflowRunsResponse, ) ) assert listing.count == len(listing.runs), ( f"/v1/workflows/runs count {listing.count} disagrees with the {len(listing.runs)} runs returned" ) @pytest.mark.covers("mgmt.credential_migration.check.happy_path") def test_credential_migration_check_reports_residual_scan(self, client: ManagementClient) -> None: report = unwrap( client.proxy.transport.get( "/credentials/migrate-encryption/check", headers=client.proxy.transport.master, params=NoBody(), response_type=MigrationCheckResponse, ) ) assert report.status == "success", f"migrate-encryption/check status {report.status!r}, expected 'success'" assert report.report.residual_legacy >= 0, ( f"residual_legacy count is negative ({report.report.residual_legacy}); the scan is broken" ) assert report.report.total_undecryptable >= 0, ( f"total_undecryptable count is negative ({report.report.total_undecryptable}); the scan is broken" ) class TestCostEstimate: @pytest.mark.covers("mgmt.cost_tracking.estimate.happy_path") def test_estimate_computes_cost_from_token_counts(self, client: ManagementClient) -> None: estimate = unwrap( client.proxy.transport.post( "/cost/estimate", headers=client.proxy.transport.master, json=CostEstimateBody( model="gpt-4o-mini", input_tokens=1000, output_tokens=500, num_requests_per_day=100 ), response_type=CostEstimateResponse, ) ) assert estimate.input_cost_per_request > 0, ( f"input cost per request is {estimate.input_cost_per_request}; a priced model must cost more than zero" ) assert estimate.output_cost_per_request > 0, ( f"output cost per request is {estimate.output_cost_per_request}; a priced model must cost more than zero" ) expected_per_request = ( estimate.input_cost_per_request + estimate.output_cost_per_request + estimate.margin_cost_per_request ) assert math.isclose(estimate.cost_per_request, expected_per_request, rel_tol=1e-9), ( f"cost_per_request {estimate.cost_per_request} != input+output+margin {expected_per_request}" ) assert estimate.daily_cost is not None and math.isclose( estimate.daily_cost, estimate.cost_per_request * 100, rel_tol=1e-9 ), f"daily_cost {estimate.daily_cost} != cost_per_request * 100 requests {estimate.cost_per_request * 100}" class TestComplianceRoutes: @pytest.mark.covers("mgmt.compliance.gdpr.happy_path") def test_gdpr_check_derives_verdict_from_the_request(self, client: ManagementClient) -> None: result = unwrap( client.proxy.transport.post( "/compliance/gdpr", headers=client.proxy.transport.master, json=ComplianceGdprBody( request_id=f"e2e-gdpr-{unique_marker()}", user_id=f"e2e-user-{unique_marker()}", model="gpt-4o-mini", timestamp="2026-07-21T00:00:00Z", ), response_type=ComplianceResponse, ) ) assert result.regulation == "GDPR", ( f"/compliance/gdpr reported regulation {result.regulation!r}, expected 'GDPR'" ) articles = {check.article for check in result.checks} assert articles == {"Art. 32", "Art. 5(1)(c)", "Art. 30"}, ( f"/compliance/gdpr returned articles {articles}, expected the three GDPR articles" ) assert result.compliant == all(check.passed for check in result.checks), ( "the overall compliant verdict must be the conjunction of the individual checks" ) assert all(check.check_name and check.detail for check in result.checks), ( "every compliance check must carry a name and a human-readable detail" ) class TestFallbackManagement: @pytest.mark.covers("mgmt.fallback_management.update.happy_path") def test_create_persists_and_is_read_back(self, client: ManagementClient, resources: ResourceManager) -> None: primary = f"e2e-fallback-primary-{unique_marker()}" secondary = f"e2e-fallback-secondary-{unique_marker()}" params = LiteLLMParamsBody(model="openai/gpt-5.5", api_key="e2e-dummy-key") primary_id = client.proxy.create_model(primary, params) resources.defer(lambda: client.proxy.delete_model(primary_id)) secondary_id = client.proxy.create_model(secondary, params) resources.defer(lambda: client.proxy.delete_model(secondary_id)) resources.defer(lambda: self._delete_fallback(client, primary)) created = unwrap( client.proxy.transport.post( "/fallback", headers=client.proxy.transport.master, json=FallbackCreateBody(model=primary, fallback_models=[secondary], fallback_type="general"), response_type=FallbackResponse, ) ) assert created.model == primary and created.fallback_models == [secondary], ( f"/fallback echoed model={created.model!r} fallbacks={created.fallback_models}, " f"configured {primary!r} -> [{secondary!r}]" ) def read_back() -> FallbackGetResponse | None: result = client.proxy.transport.get( f"/fallback/{primary}", headers=client.proxy.transport.master, params=FallbackGetParams(fallback_type="general"), response_type=FallbackGetResponse, ) match result: case Success(data=data) if secondary in data.fallback_models: return data case _: return None got = _poll(client, read_back, f"GET /fallback/{primary} never reported {secondary} after /fallback") assert got.fallback_models == [secondary], ( f"GET /fallback/{primary} reports fallbacks {got.fallback_models}, configured [{secondary!r}]" ) @staticmethod def _delete_fallback(client: ManagementClient, model: str) -> None: _ = client.proxy.transport.delete( f"/fallback/{model}", headers=client.proxy.transport.master, json=NoBody(), params=FallbackGetParams(fallback_type="general"), response_type=NoBody, ) class TestJwtKeyMapping: @pytest.mark.covers("mgmt.jwt_key_mapping.new.happy_path") def test_new_persists_mapping_and_is_read_back( self, client: ManagementClient, resources: ResourceManager ) -> None: key = client.proxy.generate_key(KeyGenerateBody()) resources.defer(lambda: client.proxy.delete_key(key)) claim_value = f"e2e_jwt_{unique_marker()}" created = unwrap( client.proxy.transport.post( "/jwt/key/mapping/new", headers=client.proxy.transport.master, json=JwtKeyMappingNewBody( jwt_claim_name="team_id", jwt_claim_value=claim_value, key=key, description="e2e coverage mapping", ), response_type=JwtKeyMappingResponse, ) ) resources.defer(lambda: self._delete_mapping(client, created.id)) assert created.jwt_claim_value == claim_value and created.is_active, ( f"/jwt/key/mapping/new returned claim_value={created.jwt_claim_value!r} active={created.is_active}, " f"configured {claim_value!r} active=True" ) info = unwrap( client.proxy.transport.get( "/jwt/key/mapping/info", headers=client.proxy.transport.master, params=JwtInfoParams(id=created.id), response_type=JwtKeyMappingResponse, ) ) assert info.id == created.id and info.jwt_claim_name == "team_id" and info.jwt_claim_value == claim_value, ( f"/jwt/key/mapping/info reports {info.jwt_claim_name!r}={info.jwt_claim_value!r} for id {info.id}, " f"created team_id={claim_value!r}" ) @staticmethod def _delete_mapping(client: ManagementClient, mapping_id: str) -> None: _ = client.proxy.transport.post( "/jwt/key/mapping/delete", headers=client.proxy.transport.master, json=JwtDeleteBody(id=mapping_id), response_type=NoBody, ) class TestRouterSettings: @pytest.mark.covers("mgmt.router_settings.update.happy_path") def test_config_update_persists_router_setting_to_get( self, client: ManagementClient, resources: ResourceManager ) -> None: """/config/update is the only write path for router_settings (there is no dedicated router-settings write route). The change is restored on teardown so the shared proxy keeps its original retry policy.""" original = self._read_num_retries(client) assert original is not None, "GET /router/settings did not report num_retries; cannot prove a change" resources.defer(lambda: self._write_num_retries(client, original)) target = original + 5 response = unwrap( client.proxy.transport.post( "/config/update", headers=client.proxy.transport.master, json=ConfigUpdateBody(router_settings=RouterSettingsPatch(num_retries=target)), response_type=ConfigUpdateResponse, ) ) assert "success" in response.message.lower(), ( f"/config/update reported {response.message!r}, expected a success message" ) _ = _poll( client, lambda: True if self._read_num_retries(client) == target else None, f"GET /router/settings never reported num_retries {target} after /config/update", ) self._write_num_retries(client, original) restored = _poll( client, lambda: original if self._read_num_retries(client) == original else None, f"GET /router/settings never returned to the original num_retries {original} after the restore", ) assert restored == original, f"router num_retries left at {restored}, expected the original {original}" @staticmethod def _read_num_retries(client: ManagementClient) -> int | None: return unwrap( client.proxy.transport.get( "/router/settings", headers=client.proxy.transport.master, params=NoBody(), response_type=RouterSettingsResponse, ) ).current_values.num_retries @staticmethod def _write_num_retries(client: ManagementClient, value: int) -> None: _ = unwrap( client.proxy.transport.post( "/config/update", headers=client.proxy.transport.master, json=ConfigUpdateBody(router_settings=RouterSettingsPatch(num_retries=value)), response_type=ConfigUpdateResponse, ) ) class TestMcpServerSubmission: @pytest.mark.covers("mgmt.mcp_server.register.happy_path") def test_register_submits_pending_server(self, client: ManagementClient, resources: ResourceManager) -> None: """A non-admin, team-scoped key submits an MCP server for review; the proxy stores it as pending_review without loading it into the runtime registry.""" team_id = client.create_team(TeamNewBody(team_alias=f"e2e-mcp-team-{unique_marker()}")) resources.defer(lambda: client.delete_team(team_id)) team_key = client.proxy.generate_key(KeyGenerateBody(team_id=team_id)) resources.defer(lambda: client.proxy.delete_key(team_key)) server_name = f"e2e_mcp_{unique_marker()}" submitted = unwrap_status( client.proxy.transport.post( "/v1/mcp/server/register", headers=client.proxy.transport.bearer(team_key), json=McpRegisterBody( server_name=server_name, url="https://example.com/mcp", transport="sse", description="e2e coverage submission", ), response_type=McpServerResponse, ), 201, ) resources.defer(lambda: self._delete_server(client, submitted.server_id)) assert submitted.approval_status == "pending_review", ( f"a user submission must be pending_review, got {submitted.approval_status!r}" ) assert submitted.server_name == server_name and submitted.transport == "sse", ( f"/v1/mcp/server/register echoed name={submitted.server_name!r} transport={submitted.transport!r}, " f"configured {server_name!r}/sse" ) @pytest.mark.covers("mgmt.mcp_server.approve.persists") def test_approve_activates_submission_and_persists( self, client: ManagementClient, resources: ResourceManager ) -> None: """An admin approving a pending submission flips it to active, and the change persists to a fresh read of the server.""" team_id = client.create_team(TeamNewBody(team_alias=f"e2e-mcp-team-{unique_marker()}")) resources.defer(lambda: client.delete_team(team_id)) team_key = client.proxy.generate_key(KeyGenerateBody(team_id=team_id)) resources.defer(lambda: client.proxy.delete_key(team_key)) submitted = unwrap( client.proxy.transport.post( "/v1/mcp/server/register", headers=client.proxy.transport.bearer(team_key), json=McpRegisterBody( server_name=f"e2e_mcp_{unique_marker()}", url="https://example.com/mcp", transport="sse", description="e2e coverage submission", ), response_type=McpServerResponse, ) ) resources.defer(lambda: self._delete_server(client, submitted.server_id)) assert submitted.approval_status == "pending_review", ( f"a fresh submission must be pending_review before approval, got {submitted.approval_status!r}" ) approved = unwrap( client.proxy.transport.put( f"/v1/mcp/server/{submitted.server_id}/approve", headers=client.proxy.transport.master, json=NoBody(), response_type=McpServerResponse, ) ) assert approved.approval_status == "active", ( f"approve must flip the submission to active, got {approved.approval_status!r}" ) fetched = unwrap( client.proxy.transport.get( f"/v1/mcp/server/{submitted.server_id}", headers=client.proxy.transport.master, params=NoBody(), response_type=McpServerResponse, ) ) assert fetched.server_id == submitted.server_id and fetched.approval_status == "active", ( f"GET /v1/mcp/server/{submitted.server_id} reports approval_status {fetched.approval_status!r} " "after approve, expected 'active'" ) @staticmethod def _delete_server(client: ManagementClient, server_id: str) -> None: _ = client.proxy.transport.delete( f"/v1/mcp/server/{server_id}", headers=client.proxy.transport.master, json=NoBody(), response_type=NoBody, )