test: align config tests with org-level v2 resolution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abhishek Kumar 2026-07-09 23:02:58 +05:30
parent 625bcd8936
commit ee26adb1db
3 changed files with 105 additions and 67 deletions

View file

@ -195,7 +195,11 @@ async def create_workflow_run_rows(
Returns:
Tuple of (workflow_run, user, workflow).
"""
from api.enums import OrganizationConfigurationKey
from api.schemas.ai_model_configuration import EffectiveAIModelConfiguration
from api.services.configuration.ai_model_configuration import (
convert_legacy_ai_model_configuration_to_v2,
)
org = OrganizationModel(provider_id=f"test-org-{provider_id_suffix}")
async_session.add(org)
@ -208,9 +212,16 @@ async def create_workflow_run_rows(
async_session.add(user)
await async_session.flush()
await db_session.update_user_configuration(
user_id=user.id,
configuration=EffectiveAIModelConfiguration.model_validate(USER_CONFIGURATION),
user_configuration = EffectiveAIModelConfiguration.model_validate(
USER_CONFIGURATION
)
await db_session.upsert_configuration(
org.id,
OrganizationConfigurationKey.MODEL_CONFIGURATION_V2.value,
convert_legacy_ai_model_configuration_to_v2(user_configuration).model_dump(
mode="json",
exclude_none=True,
),
)
workflow = await db_session.create_workflow(

View file

@ -1,3 +1,4 @@
from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
@ -7,20 +8,25 @@ from fastapi.testclient import TestClient
from api.routes.user import router
from api.schemas.ai_model_configuration import EffectiveAIModelConfiguration
from api.services.auth.depends import get_user
from api.services.configuration.ai_model_configuration import (
ResolvedAIModelConfiguration,
)
from api.services.configuration.masking import mask_key
from api.services.configuration.registry import (
GoogleLLMService,
DeepgramSTTConfiguration,
GoogleVertexLLMConfiguration,
OpenAILLMService,
OpenAITTSService,
)
def _make_test_app(selected_organization_id=None):
def _make_test_app(selected_organization_id=11):
app = FastAPI()
app.include_router(router)
mock_user = MagicMock()
mock_user.id = 1
mock_user.provider_id = "provider-1"
mock_user.is_superuser = False
mock_user.selected_organization_id = selected_organization_id
@ -38,28 +44,63 @@ def _existing_openai_config():
provider="openai",
api_key=REAL_KEY,
model="gpt-4.1",
)
),
tts=OpenAITTSService(
provider="openai",
api_key=REAL_KEY,
model="gpt-4o-mini-tts",
voice="alloy",
),
stt=DeepgramSTTConfiguration(
provider="deepgram",
api_key=REAL_KEY,
model="nova-3-general",
),
)
@contextmanager
def _patch_config_update(existing_config=None):
existing_config = existing_config or _existing_openai_config()
preferences = SimpleNamespace(test_phone_number=None, timezone=None)
with (
patch("api.routes.user.db_client") as mock_db,
patch("api.routes.user.UserConfigurationValidator") as mock_validator,
patch(
"api.routes.user.get_resolved_ai_model_configuration",
new=AsyncMock(
return_value=ResolvedAIModelConfiguration(
effective=existing_config,
source="organization_v2",
)
),
),
patch(
"api.routes.user.upsert_organization_ai_model_configuration_v2",
new=AsyncMock(),
) as upsert_config,
patch(
"api.routes.user.get_organization_preferences",
new=AsyncMock(return_value=preferences),
),
):
mock_db.get_organization_by_id = AsyncMock(return_value=None)
mock_validator.return_value.validate = AsyncMock()
yield SimpleNamespace(
db=mock_db,
validator=mock_validator,
upsert_config=upsert_config,
)
class TestMaskedKeyRejection:
def test_rejects_masked_api_key_on_provider_change(self):
"""Changing provider with a masked API key should return 400."""
app = _make_test_app()
client = TestClient(app)
with (
patch("api.routes.user.db_client") as mock_db,
patch("api.routes.user.UserConfigurationValidator") as mock_validator,
):
mock_db.get_user_configurations = AsyncMock(
return_value=_existing_openai_config()
)
mock_db.update_user_configuration = AsyncMock(
side_effect=lambda uid, cfg: cfg
)
mock_validator.return_value.validate = AsyncMock()
with _patch_config_update():
response = client.put(
"/user/configurations/user",
json={
@ -79,18 +120,7 @@ class TestMaskedKeyRejection:
app = _make_test_app()
client = TestClient(app)
with (
patch("api.routes.user.db_client") as mock_db,
patch("api.routes.user.UserConfigurationValidator") as mock_validator,
):
mock_db.get_user_configurations = AsyncMock(
return_value=_existing_openai_config()
)
mock_db.update_user_configuration = AsyncMock(
side_effect=lambda uid, cfg: cfg
)
mock_validator.return_value.validate = AsyncMock()
with _patch_config_update():
response = client.put(
"/user/configurations/user",
json={
@ -111,24 +141,7 @@ class TestMaskedKeyRejection:
client = TestClient(app)
new_key = "AIzaSyNewRealKey12345678"
updated = EffectiveAIModelConfiguration(
llm=GoogleLLMService(
provider="google",
api_key=new_key,
model="gemini-2.5-flash",
)
)
with (
patch("api.routes.user.db_client") as mock_db,
patch("api.routes.user.UserConfigurationValidator") as mock_validator,
):
mock_db.get_user_configurations = AsyncMock(
return_value=_existing_openai_config()
)
mock_db.update_user_configuration = AsyncMock(return_value=updated)
mock_validator.return_value.validate = AsyncMock()
with _patch_config_update() as patched:
response = client.put(
"/user/configurations/user",
json={
@ -141,21 +154,14 @@ class TestMaskedKeyRejection:
)
assert response.status_code == 200
patched.upsert_config.assert_awaited_once()
def test_allows_same_provider_with_masked_key(self):
"""Same provider with masked key should succeed (merge resolves it)."""
app = _make_test_app()
client = TestClient(app)
with (
patch("api.routes.user.db_client") as mock_db,
patch("api.routes.user.UserConfigurationValidator") as mock_validator,
):
existing = _existing_openai_config()
mock_db.get_user_configurations = AsyncMock(return_value=existing)
mock_db.update_user_configuration = AsyncMock(return_value=existing)
mock_validator.return_value.validate = AsyncMock()
with _patch_config_update() as patched:
response = client.put(
"/user/configurations/user",
json={
@ -170,6 +176,7 @@ class TestMaskedKeyRejection:
# Merge resolves the masked key back to the real one,
# so check_for_masked_keys should NOT raise.
assert response.status_code == 200
patched.upsert_config.assert_awaited_once()
def test_allows_same_provider_with_masked_vertex_credentials(self):
"""Same provider with masked credentials should succeed."""
@ -186,17 +193,21 @@ class TestMaskedKeyRejection:
project_id="demo-project",
location="us-east4",
credentials=real_credentials,
)
),
tts=OpenAITTSService(
provider="openai",
api_key=REAL_KEY,
model="gpt-4o-mini-tts",
voice="alloy",
),
stt=DeepgramSTTConfiguration(
provider="deepgram",
api_key=REAL_KEY,
model="nova-3-general",
),
)
with (
patch("api.routes.user.db_client") as mock_db,
patch("api.routes.user.UserConfigurationValidator") as mock_validator,
):
mock_db.get_user_configurations = AsyncMock(return_value=existing)
mock_db.update_user_configuration = AsyncMock(return_value=existing)
mock_validator.return_value.validate = AsyncMock()
with _patch_config_update(existing) as patched:
response = client.put(
"/user/configurations/user",
json={
@ -211,6 +222,7 @@ class TestMaskedKeyRejection:
)
assert response.status_code == 200
patched.upsert_config.assert_awaited_once()
def test_preference_only_update_does_not_validate_or_save_model_config(self):
"""Saving a test phone number through the legacy endpoint must not touch models."""
@ -229,6 +241,15 @@ class TestMaskedKeyRejection:
"api.routes.user.upsert_organization_preferences",
new=AsyncMock(return_value=preferences),
) as upsert_preferences,
patch(
"api.routes.user.get_resolved_ai_model_configuration",
new=AsyncMock(
return_value=ResolvedAIModelConfiguration(
effective=_existing_openai_config(),
source="organization_v2",
)
),
),
):
existing = _existing_openai_config()
mock_db.get_user_configurations = AsyncMock(return_value=existing)

View file

@ -26,7 +26,12 @@ def _embed_session():
def _embed_token(allowed_domains):
return SimpleNamespace(allowed_domains=allowed_domains, created_by=7, workflow_id=3)
return SimpleNamespace(
allowed_domains=allowed_domains,
created_by=7,
workflow_id=3,
organization_id=11,
)
def _patch_deps():
@ -35,6 +40,7 @@ def _patch_deps():
mgr = patch("api.routes.webrtc_signaling.signaling_manager").start()
db.get_embed_session_by_token = AsyncMock(return_value=_embed_session())
db.get_embed_token_by_id = AsyncMock(return_value=_embed_token(["example.com"]))
db.get_workflow_run = AsyncMock(return_value=SimpleNamespace(workflow_id=3))
db.get_user_by_id = AsyncMock(return_value=SimpleNamespace(id=7))
mgr.handle_websocket = AsyncMock()
return db, mgr