From ee26adb1dbfe5b14c9557175dd4bbc393a3b802f Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Thu, 9 Jul 2026 23:02:58 +0530 Subject: [PATCH] test: align config tests with org-level v2 resolution Co-Authored-By: Claude Fable 5 --- .../integrations/_run_pipeline_helpers.py | 17 +- api/tests/test_masked_key_rejection.py | 147 ++++++++++-------- api/tests/test_public_signaling_origin.py | 8 +- 3 files changed, 105 insertions(+), 67 deletions(-) diff --git a/api/tests/integrations/_run_pipeline_helpers.py b/api/tests/integrations/_run_pipeline_helpers.py index 6b71c110..0d5ff6c8 100644 --- a/api/tests/integrations/_run_pipeline_helpers.py +++ b/api/tests/integrations/_run_pipeline_helpers.py @@ -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( diff --git a/api/tests/test_masked_key_rejection.py b/api/tests/test_masked_key_rejection.py index 437d1cc9..1b02641a 100644 --- a/api/tests/test_masked_key_rejection.py +++ b/api/tests/test_masked_key_rejection.py @@ -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) diff --git a/api/tests/test_public_signaling_origin.py b/api/tests/test_public_signaling_origin.py index e181ad4d..a5f0a76e 100644 --- a/api/tests/test_public_signaling_origin.py +++ b/api/tests/test_public_signaling_origin.py @@ -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