99from unittest .mock import AsyncMock
1010
1111import pytest
12+ from pydantic import ValidationError
1213
1314import api .settings .endpoints as settings_api
1415from config .config_manager import (
@@ -32,10 +33,10 @@ def add_done_callback(self, cb):
3233 self .done_callback = cb
3334
3435
35- def _make_config (* , edited : bool = True ) -> OpenRAGConfig :
36+ def _make_config (* , edited : bool = True , openai : OpenAIConfig | None = None ) -> OpenRAGConfig :
3637 return OpenRAGConfig (
3738 providers = ProvidersConfig (
38- openai = OpenAIConfig (),
39+ openai = openai if openai is not None else OpenAIConfig (),
3940 anthropic = AnthropicConfig (),
4041 watsonx = WatsonXConfig (),
4142 ollama = OllamaConfig (),
@@ -67,9 +68,7 @@ def _fake_create_task(coro):
6768 lambda updated_config : True ,
6869 raising = True ,
6970 )
70- monkeypatch .setattr (
71- settings_api .clients , "refresh_patched_client" , _noop_refresh , raising = True
72- )
71+ monkeypatch .setattr (settings_api .clients , "refresh_patched_client" , _noop_refresh , raising = True )
7372 monkeypatch .setattr (settings_api .TelemetryClient , "send_event" , AsyncMock (), raising = True )
7473 monkeypatch .setattr (settings_api .asyncio , "create_task" , _fake_create_task , raising = True )
7574 return fake_task
@@ -109,6 +108,35 @@ async def test_update_settings_strips_openai_base_url_whitespace(monkeypatch):
109108 assert config .providers .openai .base_url == "https://gateway.example.com/v1"
110109
111110
111+ @pytest .mark .asyncio
112+ async def test_remove_openai_config_clears_base_url (monkeypatch ):
113+ """Regression: removing the OpenAI config used to clear api_key/configured
114+ but leave a stale base_url behind. A later re-enable of OpenAI would then
115+ silently keep routing through the old (possibly now-invalid) gateway
116+ instead of falling back to api.openai.com.
117+ """
118+ settings_api ._background_tasks .clear ()
119+ config = _make_config (
120+ openai = OpenAIConfig (
121+ api_key = "sk-test" , base_url = "https://gateway.example.com/v1" , configured = True
122+ )
123+ )
124+ # remove_openai_config requires another provider to be configured.
125+ config .providers .anthropic .configured = True
126+ _patch_update_settings_deps (monkeypatch , config )
127+
128+ response = await settings_api .update_settings (
129+ settings_api .SettingsUpdateBody (remove_openai_config = True , force_remove = True ),
130+ session_manager = object (),
131+ user = None ,
132+ )
133+
134+ assert isinstance (response , settings_api .SettingsUpdateResponse )
135+ assert config .providers .openai .api_key == ""
136+ assert config .providers .openai .configured is False
137+ assert config .providers .openai .base_url == ""
138+
139+
112140@pytest .mark .asyncio
113141async def test_onboarding_persists_openai_base_url (monkeypatch ):
114142 config = _make_config (edited = False )
@@ -123,9 +151,7 @@ async def _noop_refresh():
123151 lambda updated_config : True ,
124152 raising = True ,
125153 )
126- monkeypatch .setattr (
127- settings_api .clients , "refresh_patched_client" , _noop_refresh , raising = True
128- )
154+ monkeypatch .setattr (settings_api .clients , "refresh_patched_client" , _noop_refresh , raising = True )
129155 monkeypatch .setattr (settings_api .TelemetryClient , "send_event" , AsyncMock (), raising = True )
130156 monkeypatch .setattr (settings_api , "wait_for_langflow" , AsyncMock (), raising = True )
131157
@@ -144,3 +170,19 @@ async def _noop_refresh():
144170 assert isinstance (response , settings_api .OnboardingResponse )
145171 assert config .providers .openai .base_url == "https://gateway.example.com/v1"
146172 assert config .providers .openai .configured is False
173+
174+
175+ class TestWhitespaceOnlyBaseUrlRejected :
176+ """A whitespace-only openai_base_url must fail validation instead of being
177+ silently accepted and later stripped down to an empty string by the
178+ endpoint handlers (which would look identical to "not configured" with
179+ no error surfaced to the caller).
180+ """
181+
182+ def test_settings_update_body_rejects_whitespace_only_base_url (self ):
183+ with pytest .raises (ValidationError ):
184+ settings_api .SettingsUpdateBody (openai_base_url = " " )
185+
186+ def test_onboarding_body_rejects_whitespace_only_base_url (self ):
187+ with pytest .raises (ValidationError ):
188+ settings_api .OnboardingBody (openai_base_url = " " )
0 commit comments