Files
EvoScientist/tests/test_config_export.py

178 lines
5.9 KiB
Python

"""Contract tests for the admin config-export endpoints.
These two GET routes deliberately break the "APIs never return secrets"
invariant behind the admin-only ``config:export`` scope (config
import/export design doc, 2026-08-12).
"""
from __future__ import annotations
import time
import uuid
import jwt
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from starlette.applications import Starlette
from starlette.testclient import TestClient
from EvoScientist.image_gen import config as image_config
from EvoScientist.image_gen.config import ImageGenerationSettings, ImageModelEntry
from EvoScientist.model_registry import http_api
from EvoScientist.model_registry.auth import BffAuthenticator
from EvoScientist.model_registry.endpoint_policy import EndpointPolicy
from EvoScientist.model_registry.http_api import ApiServices, model_registry_routes
from EvoScientist.model_registry.platform import DelegationPublicKey
from EvoScientist.model_registry.resolver import ModelRegistryResolver
from EvoScientist.model_registry.snapshots import SnapshotService
from EvoScientist.model_registry.store import ModelRuntimeStore
from tests.registry_fixtures import ZHIPU_SECRET, activate_store
SERVICE_TOKEN = "bff-service-token"
_PRIVATE_KEY = ec.generate_private_key(ec.SECP256R1())
_PRIVATE_PEM = _PRIVATE_KEY.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
_PUBLIC_PEM = _PRIVATE_KEY.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
@pytest.fixture
def config_path(tmp_path):
path = tmp_path / "config.yaml"
path.write_text("other_section: {keep: true}\n", encoding="utf-8")
return path
@pytest.fixture
def services(tmp_path, config_path, monkeypatch):
store = ModelRuntimeStore(config_dir=tmp_path / "runtime")
monkeypatch.setattr(
http_api,
"load_image_generation_settings",
lambda: image_config.load_image_generation_settings(config_path=config_path),
)
resolver = ModelRegistryResolver(store)
return ApiServices(
store=store,
resolver=resolver,
snapshot_service=SnapshotService(store, resolver),
endpoint_policy=EndpointPolicy([]),
authenticator=BffAuthenticator(
service_token=SERVICE_TOKEN,
service_token_hash=None,
delegation_keys=(
DelegationPublicKey(deployment_id="webui-1", public_key=_PUBLIC_PEM),
),
jti_store=store,
),
)
@pytest.fixture
def client(services):
app = Starlette(routes=model_registry_routes(lambda: services))
return TestClient(app)
def _headers(scopes):
now = int(time.time())
claims = {
"iss": "WebUI",
"aud": "EvoScientist",
"sub": "admin-1",
"scopes": list(scopes),
"deployment_id": "webui-1",
"iat": now,
"exp": now + 30,
"jti": uuid.uuid4().hex,
}
return {
"Authorization": f"Bearer {SERVICE_TOKEN}",
"X-Evo-Actor": jwt.encode(claims, _PRIVATE_PEM, algorithm="ES256"),
}
def _export_headers():
return _headers(["config:export"])
def test_registry_export_returns_registry_with_plaintext_credentials(client, services):
activate_store(services.store)
response = client.get("/api/model-registry/export", headers=_export_headers())
assert response.status_code == 200
body = response.json()
assert body["kind"] == "evoscientist.model-registry"
assert body["format_version"] == 1
assert isinstance(body["exported_at"], str) and body["exported_at"]
provider_ids = {p["id"] for p in body["payload"]["registry"]["providers"]}
assert provider_ids == {"zhipu-glm", "local-ollama"}
assert body["payload"]["credentials"] == [
{"credential_id": "zhipu-primary", "secret_value": ZHIPU_SECRET}
]
def test_registry_export_empty_store_has_empty_credentials(client):
response = client.get("/api/model-registry/export", headers=_export_headers())
assert response.status_code == 200
assert response.json()["payload"]["credentials"] == []
def test_registry_export_requires_export_scope(client, services):
activate_store(services.store)
response = client.get(
"/api/model-registry/export",
headers=_headers(["model_config:read", "model_config:write"]),
)
assert response.status_code == 403
assert response.json()["code"] == "FORBIDDEN"
def test_registry_export_requires_delegation(client):
response = client.get(
"/api/model-registry/export",
headers={"Authorization": f"Bearer {SERVICE_TOKEN}"},
)
assert response.status_code == 401
def test_image_export_returns_plaintext_api_keys(client, config_path):
settings = ImageGenerationSettings(
default_model="gpt-image-2",
timeout_seconds=60.0,
models=[
ImageModelEntry(
id="gpt-image-2",
name="GPT Image",
provider="openai",
api_key="sk-image-secret",
base_url="https://api.example.com/v1",
)
],
)
image_config.save_image_generation_settings(settings, config_path=config_path)
response = client.get("/api/image-generation/export", headers=_export_headers())
assert response.status_code == 200
body = response.json()
assert body["kind"] == "evoscientist.image-generation"
assert body["format_version"] == 1
payload = body["payload"]
assert payload["default_model"] == "gpt-image-2"
assert payload["timeout_seconds"] == 60.0
assert payload["models"][0]["api_key"] == "sk-image-secret"
assert "api_key_configured" not in payload["models"][0]
def test_image_export_requires_export_scope(client):
response = client.get(
"/api/image-generation/export",
headers=_headers(["model_config:read", "model_config:write"]),
)
assert response.status_code == 403