mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
test: align config tests with org-level v2 resolution
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
625bcd8936
commit
ee26adb1db
3 changed files with 105 additions and 67 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue