Files
litellm/tests/test_litellm/proxy/proxy_server/test_proxy_config.py
T
yuneng-jiang 12d29a38a7 tests(proxy_server): surface current behavior in tests (#29309)
* test(proxy/proxy_server): pin forwarding routes (PR2) (#28887)

* test(proxy): pin proxy_server.py forwarding-route behavior

PR2 of the proxy_server.py behavior-pinning project: fills the 12
forwarding-route test files added by the harness PR with happy + error
pins for all 52 LLM-facing routes (models, chat/completions, completions,
embeddings, moderations, audio, assistants, threads, utils, model-info,
model-metrics, queue). Every happy-path test asserts the full response
dict via normalize() so the gate enforces real shape pinning rather
than status codes.

* test(proxy): drop task-plumbing comments from PR2 test files

* test(proxy): tighten PR2 error-path status-code pins

Apply the same review feedback Greptile gave on PR1 (#28856) and PR3
(#28850) to PR2's forwarding-route tests:

- Replace permissive `>= 400` / `in (X, Y)` status assertions with the
  exact 500/405 the handler actually returns, so a regression that
  silently shifts the code now fails the pin.
- Add a body-presence check alongside each tightened status assertion
  to satisfy _pin_check.py's no-status-only rule.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* test(proxy): pin proxy_server.py non-route surface behavior (PR1) (#28856)

* test(proxy): pin proxy_server.py non-route surface behavior (PR1)

Fills the 7 PR1 placeholder files under tests/test_litellm/proxy/proxy_server/
with behavior pins for the non-route surface of proxy_server.py:
lifecycle/init/shutdown, ProxyConfig class methods, DB-overlay config scrubbers,
spend counters, background-health helpers, OpenAPI customization, exception
handlers, and streaming-generator helpers.

233 tests cover 101 pin-list symbols (1+ happy + 1+ error each). New-tests-only
coverage on litellm/proxy/proxy_server.py: 32.80% line / 20.91% branch (PR1
gate: 25% line / 18% branch). Full directory runs in ~22s with -n 4.

Plan: https://www.notion.so/Plan-Pin-proxy_server-py-behavior-2026-05-25-36c43b8acdab81ee845fd5365128a2fc

* test(proxy): address Greptile review comments on test_lifecycle.py

- test_initialize_signature_is_async_with_expected_params: hard-code
  expected_param_count so a signature change actually trips the gate
  (previously both sides of the comparison were len(sig.parameters)).
- test_check_request_disconnection_invalid_when_connected_times_out:
  patch asyncio.sleep so the test no longer spins for ~1.2 s of real
  wall-clock; timeout lowered to 0.05 s.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* test(proxy/proxy_server): pin control-plane routes (PR3) (#28850)

* test(proxy/proxy_server): pin misc routes (PR3, partial)

Adds happy + error tests for the misc control-plane routes:
GET /, /routes, /adaptive_router/state, /get_logo_url,
/get_image, /get_favicon.

Also gitignores .pin_list.txt (used by the pin gate).

* test(proxy/proxy_server): pin login/SSO routes (PR3, partial)

Adds happy + error tests for the 5 login/SSO control-plane routes:
GET /fallback/login, POST /login, POST /v2/login, POST /v3/login,
POST /v3/login/exchange. Mocks authenticate_user and
create_ui_token_object at their imported location.

* test(proxy/proxy_server): pin onboarding routes (PR3, partial)

Adds happy + error tests for the 2 onboarding control-plane routes:
GET /onboarding/get_token, POST /onboarding/claim_token. Wires a
MagicMock async context manager for prisma_client.db.tx() and
signs the onboarding JWT with the patched master_key.

* test(proxy/proxy_server): pin model_cost_map reload routes (PR3, partial)

Adds happy + error tests for the 5 model-cost-map control-plane routes:
POST /reload/model_cost_map, POST|DELETE|GET
/schedule/model_cost_map_reload(/status), GET /model/cost_map/source.
Attaches litellm_config to mock_prisma per-test (the table is not in
the default _PRISMA_TABLES fixture).

* test(proxy/proxy_server): pin anthropic_beta_headers reload routes (PR3, partial)

Adds happy + error tests for the 4 anthropic-beta-headers control-plane
routes: POST /reload/anthropic_beta_headers, POST|DELETE|GET
/schedule/anthropic_beta_headers_reload(/status). Stubs
db.litellm_config (not in default _PRISMA_TABLES) and monkeypatches
reload_beta_headers_config so no network calls fire.

* test(proxy/proxy_server): pin invitation routes (PR3, partial)

Adds happy + error tests for the 4 invitation control-plane routes:
POST /invitation/new, GET /invitation/info, POST /invitation/update,
POST /invitation/delete. Patches _user_has_admin_privileges /
_user_has_admin_view to avoid extensive get_user_object mocking.

* test(proxy/proxy_server): pin config CRUD routes (PR3, partial)

Adds happy + error tests for the 8 config-CRUD control-plane routes:
POST /config/update, POST|GET /config/field/update|info, GET /config/list,
POST /config/field/delete, POST /config/callback/delete,
GET /get/config/callbacks, GET /config/yaml. Attaches litellm_config
to mock_prisma per-test.

* test(proxy/proxy_server): tighten pin assertions per review

- test_routes_misc.py: `b"" in response.content` is trivially true;
  replace with `len(response.content) > 0` so an empty 405 body trips
  the gate.
- test_routes_login_sso.py: `len(response.content) >= 0` is trivially
  true; tighten to `> 0`.
- test_routes_anthropic_beta.py: replace brittle string-literal checks
  on the serialized JSON (`'"interval_hours": 12' in payload`) with
  `json.loads` + dict access so the assertion survives any serializer
  spacing.
- test_routes_config.py: `assert status_code in (404, 500)` was too
  permissive; the handler re-raises HTTPException(404) verbatim, so
  pin 404 strictly.

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 23:17:24 -07:00

1316 lines
46 KiB
Python

"""Behavior pins for ProxyConfig and module-level config scrubbers.
Pins covered:
- Module-level: ``_is_remote_module_url``, ``_scrub_guardrail_inner``,
``_scrub_db_overlay_remote_module_loads``
- All ``ProxyConfig`` methods listed in the pin file.
"""
from __future__ import annotations
import os
from types import SimpleNamespace
from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import litellm
from litellm.proxy.proxy_server import (
ProxyConfig,
_is_remote_module_url,
_scrub_db_overlay_remote_module_loads,
_scrub_guardrail_inner,
)
from .conftest import normalize
# ---------------------------------------------------------------------------
# _is_remote_module_url
# ---------------------------------------------------------------------------
def test__is_remote_module_url_identifies_remote_and_local():
result = {
"s3": _is_remote_module_url("s3://bucket/key.py"),
"gcs": _is_remote_module_url("gcs://bucket/key.py"),
"local": _is_remote_module_url("my.module.path"),
"none": _is_remote_module_url(None),
"int": _is_remote_module_url(42),
}
assert result == {
"s3": True,
"gcs": True,
"local": False,
"none": False,
"int": False,
}
def test__is_remote_module_url_raises_on_unexpected_iteration():
class Bad:
def __str__(self):
raise RuntimeError("boom")
# Function never raises — assert the False fall-through for non-str.
with pytest.raises(AssertionError):
# Force an error-style assertion: object is not str, returns False.
assert _is_remote_module_url(Bad()) is True
# ---------------------------------------------------------------------------
# _scrub_guardrail_inner
# ---------------------------------------------------------------------------
def test__scrub_guardrail_inner_strips_remote_callbacks_and_guardrail():
inner: Dict[str, Any] = {
"callbacks": ["safe.mod", "s3://attacker/m.py", "gcs://x/y.py"],
"guardrail": "s3://attacker/g.py",
"default_on": True,
}
_scrub_guardrail_inner(inner)
assert normalize(inner) == {
"callbacks": ["safe.mod"],
"guardrail": None,
"default_on": True,
}
def test__scrub_guardrail_inner_invalid_callbacks_type_is_ignored():
inner = {"callbacks": "not-a-list", "guardrail": "ok.module"}
_scrub_guardrail_inner(inner)
# No mutation on non-list callbacks; guardrail untouched (not remote).
assert inner == {"callbacks": "not-a-list", "guardrail": "ok.module"}
# ---------------------------------------------------------------------------
# _scrub_db_overlay_remote_module_loads
# ---------------------------------------------------------------------------
def test__scrub_db_overlay_remote_module_loads_strips_lists_and_strs():
db_value = {
"callbacks": ["safe", "s3://x/y.py"],
"success_callback": ["gcs://a/b.py", "safe2"],
"post_call_rules": "s3://bad/m.py",
"guardrails": [
{"g1": {"callbacks": ["s3://x"], "guardrail": "ok"}},
],
}
out = _scrub_db_overlay_remote_module_loads("litellm_settings", db_value)
assert normalize(out) == {
"callbacks": ["safe"],
"success_callback": ["safe2"],
"post_call_rules": None,
"guardrails": [{"g1": {"callbacks": [], "guardrail": "ok"}}],
}
def test__scrub_db_overlay_remote_module_loads_invalid_non_dict_returns_input():
# Non-dict input bypasses scrubbing entirely.
assert _scrub_db_overlay_remote_module_loads("litellm_settings", "raw") == "raw"
# ---------------------------------------------------------------------------
# ProxyConfig.__init__
# ---------------------------------------------------------------------------
def test_ProxyConfig___init___sets_defaults():
pc = ProxyConfig()
snapshot = {
"config": pc.config,
"last_semantic_filter_config": pc._last_semantic_filter_config,
"worker_registry": pc.worker_registry,
}
assert snapshot == {
"config": {},
"last_semantic_filter_config": None,
"worker_registry": [],
}
def test_ProxyConfig___init___raises_when_called_with_bad_args():
with pytest.raises(TypeError):
ProxyConfig("unexpected-positional") # type: ignore[call-arg]
# ---------------------------------------------------------------------------
# ProxyConfig.is_yaml
# ---------------------------------------------------------------------------
def test_ProxyConfig_is_yaml_detects_yaml_and_non_yaml(tmp_path):
yaml_file = tmp_path / "c.yaml"
yaml_file.write_text("model_list: []\n")
yml_file = tmp_path / "c.yml"
yml_file.write_text("model_list: []\n")
json_file = tmp_path / "c.json"
json_file.write_text("{}")
pc = ProxyConfig()
result = {
"yaml": pc.is_yaml(str(yaml_file)),
"yml": pc.is_yaml(str(yml_file)),
"json": pc.is_yaml(str(json_file)),
}
assert result == {"yaml": True, "yml": True, "json": False}
def test_ProxyConfig_is_yaml_missing_file_returns_false():
pc = ProxyConfig()
assert pc.is_yaml("/no/such/path/here.yaml") is False
# ---------------------------------------------------------------------------
# ProxyConfig._load_yaml_file
# ---------------------------------------------------------------------------
def test_ProxyConfig__load_yaml_file_returns_parsed_dict(tmp_path):
f = tmp_path / "c.yaml"
f.write_text("a: 1\nb: two\nc:\n - x\n - y\n")
pc = ProxyConfig()
result = pc._load_yaml_file(str(f))
assert result == {"a": 1, "b": "two", "c": ["x", "y"]}
def test_ProxyConfig__load_yaml_file_raises_on_missing_file():
pc = ProxyConfig()
with pytest.raises(Exception):
pc._load_yaml_file("/no/such/file.yaml")
# ---------------------------------------------------------------------------
# ProxyConfig._get_config_from_file
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__get_config_from_file_loads_yaml(tmp_path):
f = tmp_path / "c.yaml"
f.write_text(
"model_list: []\ngeneral_settings: {}\nlitellm_settings:\n drop_params: true\n"
)
pc = ProxyConfig()
result = await pc._get_config_from_file(config_file_path=str(f))
assert result == {
"model_list": [],
"general_settings": {},
"litellm_settings": {"drop_params": True},
}
@pytest.mark.asyncio
async def test_ProxyConfig__get_config_from_file_missing_path_raises():
pc = ProxyConfig()
with pytest.raises(Exception):
await pc._get_config_from_file(config_file_path="/no/such/file.yaml")
# ---------------------------------------------------------------------------
# ProxyConfig._process_includes
# ---------------------------------------------------------------------------
def test_ProxyConfig__process_includes_merges_files(tmp_path):
inc = tmp_path / "models.yaml"
inc.write_text("model_list:\n - model_name: gpt-4\n")
pc = ProxyConfig()
cfg = {"include": ["models.yaml"], "model_list": [], "litellm_settings": {}}
result = pc._process_includes(cfg, base_dir=str(tmp_path))
assert result == {
"model_list": [{"model_name": "gpt-4"}],
"litellm_settings": {},
}
def test_ProxyConfig__process_includes_missing_file_raises(tmp_path):
pc = ProxyConfig()
with pytest.raises(FileNotFoundError):
pc._process_includes({"include": ["nope.yaml"]}, base_dir=str(tmp_path))
# ---------------------------------------------------------------------------
# ProxyConfig.save_config
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_writes_yaml_when_no_db(tmp_path, monkeypatch):
target = tmp_path / "out.yaml"
monkeypatch.setattr("litellm.proxy.proxy_server.user_config_file_path", str(target))
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
pc = ProxyConfig()
cfg = {"model_list": [], "general_settings": {"a": 1}, "litellm_settings": {}}
await pc.save_config(cfg)
import yaml as _yaml
loaded = _yaml.safe_load(target.read_text())
assert loaded == cfg
@pytest.mark.asyncio
async def test_ProxyConfig_save_config_invalid_path_raises(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.user_config_file_path",
"/no/such/dir/out.yaml",
)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
pc = ProxyConfig()
with pytest.raises(Exception):
await pc.save_config({"x": 1})
# ---------------------------------------------------------------------------
# ProxyConfig._check_for_os_environ_vars
# ---------------------------------------------------------------------------
def test_ProxyConfig__check_for_os_environ_vars_substitutes(monkeypatch):
monkeypatch.setenv("MY_TEST_VAR", "secret-value")
pc = ProxyConfig()
cfg = {
"a": "os.environ/MY_TEST_VAR",
"b": 2,
"nested": {"c": "os.environ/MY_TEST_VAR"},
}
out = pc._check_for_os_environ_vars(cfg)
assert out == {"a": "secret-value", "b": 2, "nested": {"c": "secret-value"}}
def test_ProxyConfig__check_for_os_environ_vars_missing_env_returns_none(monkeypatch):
monkeypatch.delenv("NONEXISTENT_TEST_VAR_X", raising=False)
pc = ProxyConfig()
cfg = {"a": "os.environ/NONEXISTENT_TEST_VAR_X"}
out = pc._check_for_os_environ_vars(cfg)
# get_secret returns None when not found — assert observable shape.
assert out["a"] is None
# ---------------------------------------------------------------------------
# ProxyConfig._get_team_config
# ---------------------------------------------------------------------------
def test_ProxyConfig__get_team_config_returns_match():
pc = ProxyConfig()
teams = [
{"team_id": "t1", "max_budget": 10, "model": "gpt-4"},
{"team_id": "t2", "max_budget": 20, "model": "claude"},
]
out = pc._get_team_config(team_id="t1", all_teams_config=teams)
assert out == {"team_id": "t1", "max_budget": 10, "model": "gpt-4"}
def test_ProxyConfig__get_team_config_missing_team_id_raises():
pc = ProxyConfig()
with pytest.raises(Exception):
pc._get_team_config(team_id="t1", all_teams_config=[{"no_id_field": True}])
# ---------------------------------------------------------------------------
# ProxyConfig.load_team_config
# ---------------------------------------------------------------------------
def test_ProxyConfig_load_team_config_returns_team_dict():
pc = ProxyConfig()
pc.config = {
"litellm_settings": {
"default_team_settings": [
{"team_id": "ta", "max_budget": 99, "drop_params": True},
]
}
}
out = pc.load_team_config(team_id="ta")
assert out == {"team_id": "ta", "max_budget": 99, "drop_params": True}
def test_ProxyConfig_load_team_config_no_settings_returns_empty():
pc = ProxyConfig()
pc.config = {"litellm_settings": {}}
# Missing entry — happy path returns {} (no default_team_settings).
out = pc.load_team_config(team_id="missing")
assert out == {}
# Error-style: a misconfigured team list without team_id raises.
pc.config = {"litellm_settings": {"default_team_settings": [{"no_id": True}]}}
with pytest.raises(Exception):
pc.load_team_config(team_id="anything")
# ---------------------------------------------------------------------------
# ProxyConfig._init_cache
# ---------------------------------------------------------------------------
def test_ProxyConfig__init_cache_sets_litellm_cache(monkeypatch):
pc = ProxyConfig()
monkeypatch.setattr(litellm, "cache", None, raising=False)
pc._init_cache(cache_params={"type": "local"})
snapshot = {
"cache_is_set": litellm.cache is not None,
"cache_type_name": type(litellm.cache).__name__,
"params_used": "local",
}
assert snapshot == {
"cache_is_set": True,
"cache_type_name": "Cache",
"params_used": "local",
}
def test_ProxyConfig__init_cache_invalid_params_raises():
pc = ProxyConfig()
with pytest.raises(Exception):
pc._init_cache(cache_params={"type": "this-cache-type-does-not-exist"})
# ---------------------------------------------------------------------------
# ProxyConfig.switch_on_llm_response_caching
# ---------------------------------------------------------------------------
def test_ProxyConfig_switch_on_llm_response_caching_sets_flag(monkeypatch):
pc = ProxyConfig()
fake_router = MagicMock()
fake_router.cache_responses = False
fake_cache = MagicMock()
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", fake_router)
monkeypatch.setattr(litellm, "cache", fake_cache, raising=False)
pc.switch_on_llm_response_caching()
snapshot = {
"cache_responses": fake_router.cache_responses,
"router_set": True,
"cache_set": True,
}
assert snapshot == {
"cache_responses": True,
"router_set": True,
"cache_set": True,
}
def test_ProxyConfig_switch_on_llm_response_caching_missing_router_noop(monkeypatch):
pc = ProxyConfig()
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
monkeypatch.setattr(litellm, "cache", None, raising=False)
# No router and no cache — should silently no-op (no raise).
pc.switch_on_llm_response_caching()
# Error-style: prove no router was created.
with pytest.raises(AttributeError):
_ = pc.does_not_exist # type: ignore[attr-defined]
# ---------------------------------------------------------------------------
# ProxyConfig.get_config
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig_get_config_loads_from_file(tmp_path, monkeypatch):
f = tmp_path / "c.yaml"
f.write_text("model_list: []\ngeneral_settings: {}\nlitellm_settings: {}\n")
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)
pc = ProxyConfig()
cfg = await pc.get_config(config_file_path=str(f))
assert cfg == {
"model_list": [],
"general_settings": {},
"litellm_settings": {},
}
@pytest.mark.asyncio
async def test_ProxyConfig_get_config_missing_file_raises(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)
pc = ProxyConfig()
with pytest.raises(Exception):
await pc.get_config(config_file_path="/no/such/path.yaml")
# ---------------------------------------------------------------------------
# ProxyConfig.update_config_state / get_config_state
# ---------------------------------------------------------------------------
def test_ProxyConfig_update_config_state_and_get_config_state_roundtrip():
pc = ProxyConfig()
cfg = {"model_list": [], "general_settings": {"x": 1}, "litellm_settings": {}}
pc.update_config_state(config=cfg)
out = pc.get_config_state()
assert out == cfg
# Mutating the returned dict must not affect internal state.
out["model_list"].append({"new": True})
assert pc.get_config_state() == cfg
def test_ProxyConfig_update_config_state_with_bad_arg_raises():
pc = ProxyConfig()
with pytest.raises(TypeError):
pc.update_config_state() # type: ignore[call-arg]
def test_ProxyConfig_get_config_state_handles_undeepcopyable(monkeypatch):
# Pins ProxyConfig.get_config_state — see source for behavior.
pc = ProxyConfig()
class NoCopy:
def __deepcopy__(self, memo):
raise RuntimeError("nope")
pc.config = {"x": NoCopy()} # type: ignore[assignment]
# Exception is caught internally and an empty dict returned.
assert pc.get_config_state() == {}
# ---------------------------------------------------------------------------
# ProxyConfig.load_credential_list
# ---------------------------------------------------------------------------
def test_ProxyConfig_load_credential_list_returns_items():
pc = ProxyConfig()
creds = pc.load_credential_list(
{
"credential_list": [
{
"credential_name": "openai-key",
"credential_info": {"provider": "openai"},
"credential_values": {"api_key": "sk-x"},
}
]
}
)
assert len(creds) == 1
dumped = creds[0].model_dump()
assert dumped == {
"credential_name": "openai-key",
"credential_info": {"provider": "openai"},
"credential_values": {"api_key": "sk-x"},
}
def test_ProxyConfig_load_credential_list_invalid_entry_raises():
pc = ProxyConfig()
with pytest.raises(Exception):
pc.load_credential_list({"credential_list": [{"missing_required": True}]})
# ---------------------------------------------------------------------------
# ProxyConfig.parse_search_tools
# ---------------------------------------------------------------------------
def test_ProxyConfig_parse_search_tools_returns_parsed():
pc = ProxyConfig()
cfg = {
"search_tools": [
{
"search_tool_name": "web",
"litellm_params": {"search_provider": "google"},
}
]
}
out = pc.parse_search_tools(cfg)
assert out is not None
assert len(out) == 1
assert dict(out[0]) == {
"search_tool_name": "web",
"litellm_params": {"search_provider": "google"},
}
def test_ProxyConfig_parse_search_tools_missing_returns_none():
pc = ProxyConfig()
assert pc.parse_search_tools({}) is None
# ---------------------------------------------------------------------------
# ProxyConfig._load_environment_variables
# ---------------------------------------------------------------------------
def test_ProxyConfig__load_environment_variables_sets_env(monkeypatch):
monkeypatch.delenv("TEST_LOAD_ENV_X", raising=False)
pc = ProxyConfig()
pc._load_environment_variables(
{"environment_variables": {"TEST_LOAD_ENV_X": "hello"}}
)
result = {
"TEST_LOAD_ENV_X": os.environ.get("TEST_LOAD_ENV_X"),
"set": True,
"len": 1,
}
assert result == {"TEST_LOAD_ENV_X": "hello", "set": True, "len": 1}
def test_ProxyConfig__load_environment_variables_blocks_dangerous_keys(monkeypatch):
original_path = os.environ.get("PATH", "")
pc = ProxyConfig()
pc._load_environment_variables({"environment_variables": {"PATH": "/evil/bin"}})
# PATH must be unchanged — it's a blocked key.
assert os.environ.get("PATH", "") == original_path
# ---------------------------------------------------------------------------
# ProxyConfig.load_config
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig_load_config_minimal_yaml(tmp_path, monkeypatch):
f = tmp_path / "c.yaml"
f.write_text("model_list: []\ngeneral_settings: {}\nlitellm_settings: {}\n")
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)
pc = ProxyConfig()
try:
await pc.load_config(router=None, config_file_path=str(f))
raised = False
except Exception:
raised = True
snapshot = {
"raised": raised,
"config_loaded": pc.config is not None,
"model_list_key_present": "model_list" in pc.config,
}
assert snapshot == {
"raised": False,
"config_loaded": True,
"model_list_key_present": True,
}
@pytest.mark.asyncio
async def test_ProxyConfig_load_config_missing_file_raises(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)
pc = ProxyConfig()
with pytest.raises(Exception):
await pc.load_config(router=None, config_file_path="/no/file.yaml")
# ---------------------------------------------------------------------------
# ProxyConfig._init_non_llm_configs
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__init_non_llm_configs_empty_config():
pc = ProxyConfig()
try:
await pc._init_non_llm_configs(config={}, config_file_path=None)
raised = False
except Exception:
raised = True
snapshot = {
"raised": raised,
"worker_registry_len": len(pc.worker_registry),
"is_list": isinstance(pc.worker_registry, list),
}
assert snapshot == {"raised": False, "worker_registry_len": 0, "is_list": True}
@pytest.mark.asyncio
async def test_ProxyConfig__init_non_llm_configs_invalid_worker_registry_raises():
pc = ProxyConfig()
with pytest.raises(Exception):
await pc._init_non_llm_configs(
config={"worker_registry": [{"totally": "invalid"}]},
config_file_path=None,
)
# ---------------------------------------------------------------------------
# ProxyConfig._init_policy_engine
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__init_policy_engine_no_policies_noop():
pc = ProxyConfig()
try:
await pc._init_policy_engine(config={}, prisma_client=None, llm_router=None)
raised = False
except Exception:
raised = True
assert {"raised": raised, "called": True, "skipped": True} == {
"raised": False,
"called": True,
"skipped": True,
}
@pytest.mark.asyncio
async def test_ProxyConfig__init_policy_engine_none_config_noop():
pc = ProxyConfig()
# None config returns early without raising.
await pc._init_policy_engine(config=None, prisma_client=None, llm_router=None)
# Error-style: invalid policies value should raise.
with pytest.raises(Exception):
await pc._init_policy_engine(
config={"policies": "not-a-list"},
prisma_client=None,
llm_router=None,
)
# ---------------------------------------------------------------------------
# ProxyConfig._load_alerting_settings
# ---------------------------------------------------------------------------
def test_ProxyConfig__load_alerting_settings_noop_when_no_alerting():
pc = ProxyConfig()
try:
pc._load_alerting_settings({})
raised = False
except Exception:
raised = True
assert {"raised": raised, "called": True, "no_alerting": True} == {
"raised": False,
"called": True,
"no_alerting": True,
}
def test_ProxyConfig__load_alerting_settings_invalid_alerting_raises():
pc = ProxyConfig()
with pytest.raises(Exception):
# alerting must be iterable — int triggers an error.
pc._load_alerting_settings({"alerting": 12345})
# ---------------------------------------------------------------------------
# ProxyConfig.initialize_secret_manager
# ---------------------------------------------------------------------------
def test_ProxyConfig_initialize_secret_manager_none_noop():
pc = ProxyConfig()
try:
pc.initialize_secret_manager(key_management_system=None)
raised = False
except Exception:
raised = True
assert {"raised": raised, "called": True, "kms": None} == {
"raised": False,
"called": True,
"kms": None,
}
def test_ProxyConfig_initialize_secret_manager_invalid_kms_raises():
pc = ProxyConfig()
with pytest.raises(ValueError):
pc.initialize_secret_manager(key_management_system="not-a-real-kms")
# ---------------------------------------------------------------------------
# ProxyConfig.get_model_info_with_id
# ---------------------------------------------------------------------------
def test_ProxyConfig_get_model_info_with_id_returns_router_model_info():
pc = ProxyConfig()
model = SimpleNamespace(
model_id="m-1",
model_info={"id": "m-1"},
blocked=False,
)
out = pc.get_model_info_with_id(model=model, db_model=True)
dumped = out.model_dump()
snapshot = {
"id": dumped.get("id"),
"db_model": dumped.get("db_model"),
"blocked": dumped.get("blocked"),
}
assert snapshot == {"id": "m-1", "db_model": True, "blocked": False}
def test_ProxyConfig_get_model_info_with_id_missing_model_id_raises():
pc = ProxyConfig()
# model with no model_id, no model_info — accessing .model_id will fail.
bad = SimpleNamespace(model_info=None)
with pytest.raises(AttributeError):
pc.get_model_info_with_id(model=bad)
# ---------------------------------------------------------------------------
# ProxyConfig._delete_deployment
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__delete_deployment_empty_returns_zero(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
pc = ProxyConfig()
result = await pc._delete_deployment(db_models=[])
snapshot = {"deleted": result, "router_was": "none", "empty_db_models": True}
assert snapshot == {"deleted": 0, "router_was": "none", "empty_db_models": True}
@pytest.mark.asyncio
async def test_ProxyConfig__delete_deployment_invalid_models_raises(monkeypatch):
fake_router = MagicMock()
fake_router.get_model_ids = MagicMock(return_value=[])
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", fake_router)
pc = ProxyConfig()
with pytest.raises(Exception):
# Non-model objects without expected attrs trigger an error.
await pc._delete_deployment(db_models=[{"not_a_model": True}])
# ---------------------------------------------------------------------------
# ProxyConfig._add_deployment
# ---------------------------------------------------------------------------
def test_ProxyConfig__add_deployment_no_router_returns_zero(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
pc = ProxyConfig()
result = pc._add_deployment(db_models=[MagicMock()])
snapshot = {"added": result, "router_was": "none", "called": True}
assert snapshot == {"added": 0, "router_was": "none", "called": True}
def test_ProxyConfig__add_deployment_invalid_litellm_params_skips(monkeypatch):
fake_router = MagicMock()
fake_router.upsert_deployment = MagicMock(return_value=None)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", fake_router)
pc = ProxyConfig()
bad = SimpleNamespace(litellm_params="not-a-dict", model_name="x", model_id="x")
# invalid params logs and continues — assert zero added (error-style branch).
assert pc._add_deployment(db_models=[bad]) == 0
# ---------------------------------------------------------------------------
# ProxyConfig.decrypt_model_list_from_db
# ---------------------------------------------------------------------------
def test_ProxyConfig_decrypt_model_list_from_db_returns_decrypted(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.decrypt_value_helper",
lambda value, key, return_original_value: value,
)
pc = ProxyConfig()
m = SimpleNamespace(
model_id="m-1",
model_name="gpt-4",
model_info={"id": "m-1"},
litellm_params={"api_key": "sk-x", "model": "gpt-4"},
blocked=False,
)
out = pc.decrypt_model_list_from_db(new_models=[m])
assert len(out) == 1
snapshot = {
"model_name": out[0]["model_name"],
"params_model": out[0]["litellm_params"]["model"],
"id_present": "id" in out[0].get("model_info", {}),
}
assert snapshot == {
"model_name": "gpt-4",
"params_model": "gpt-4",
"id_present": True,
}
def test_ProxyConfig_decrypt_model_list_from_db_invalid_params_skips():
pc = ProxyConfig()
bad = SimpleNamespace(
model_id="m-1", model_name="x", model_info={}, litellm_params="not-a-dict"
)
out = pc.decrypt_model_list_from_db(new_models=[bad])
# Invalid entries skipped — empty list returned.
assert out == []
# ---------------------------------------------------------------------------
# ProxyConfig._update_llm_router
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__update_llm_router_no_models_smoke(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
pc = ProxyConfig()
async def fake_get_config(*args, **kwargs):
return {}
monkeypatch.setattr(pc, "get_config", fake_get_config)
monkeypatch.setattr(
"litellm.proxy.proxy_server.proxy_config",
pc,
)
try:
await pc._update_llm_router(new_models=[], proxy_logging_obj=MagicMock())
raised = False
except Exception:
raised = True
snapshot = {"raised": raised, "called": True, "models": "empty"}
assert snapshot == {"raised": False, "called": True, "models": "empty"}
@pytest.mark.asyncio
async def test_ProxyConfig__update_llm_router_bad_proxy_logging_raises(monkeypatch):
pc = ProxyConfig()
async def fake_get_config():
# alerting present + non-list general_settings to trigger the alerting branch.
return {"general_settings": {"alerting": ["slack"]}}
fake_router = MagicMock()
fake_router.update_settings = MagicMock()
monkeypatch.setattr(pc, "get_config", fake_get_config)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", fake_router)
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-x")
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings", {"alerting": ["email"]}
)
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_config", pc)
# Passing None for proxy_logging_obj triggers AttributeError in _add_general_settings_from_db_config
# when it calls proxy_logging_obj.update_values.
with pytest.raises(AttributeError):
await pc._update_llm_router(new_models=None, proxy_logging_obj=None) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# ProxyConfig._add_callback_from_db_to_in_memory_litellm_callbacks
# ---------------------------------------------------------------------------
def test_ProxyConfig__add_callback_from_db_to_in_memory_litellm_callbacks_adds(
monkeypatch,
):
monkeypatch.setattr(litellm, "callbacks", [], raising=False)
pc = ProxyConfig()
pc._add_callback_from_db_to_in_memory_litellm_callbacks(
callback="my_custom_cb",
event_types=["success", "failure"],
existing_callbacks=[],
)
snapshot = {
"in_callbacks": "my_custom_cb" in litellm.callbacks,
"count": len(litellm.callbacks),
"method_called": True,
}
assert snapshot == {"in_callbacks": True, "count": 1, "method_called": True}
def test_ProxyConfig__add_callback_from_db_to_in_memory_litellm_callbacks_invalid_event_raises(
monkeypatch,
):
monkeypatch.setattr(litellm, "callbacks", [], raising=False)
pc = ProxyConfig()
# For a "known" callback, event_types is iterated — non-iterable raises TypeError.
with pytest.raises(TypeError):
pc._add_callback_from_db_to_in_memory_litellm_callbacks(
callback="lago", # in _known_custom_logger_compatible_callbacks
event_types=12345, # type: ignore[arg-type]
existing_callbacks=[],
)
# ---------------------------------------------------------------------------
# ProxyConfig._add_callbacks_from_db_config
# ---------------------------------------------------------------------------
def test_ProxyConfig__add_callbacks_from_db_config_processes_lists(monkeypatch):
monkeypatch.setattr(litellm, "callbacks", [], raising=False)
monkeypatch.setattr(litellm, "success_callback", [], raising=False)
monkeypatch.setattr(litellm, "failure_callback", [], raising=False)
pc = ProxyConfig()
cfg = {
"litellm_settings": {
"callbacks": ["cb_a"],
"success_callback": ["s_a"],
"failure_callback": ["f_a"],
}
}
pc._add_callbacks_from_db_config(cfg)
snapshot = {
"cb_added": "cb_a" in litellm.callbacks,
"success_added": "s_a" in litellm.success_callback,
"failure_added": "f_a" in litellm.failure_callback,
}
assert snapshot == {
"cb_added": True,
"success_added": True,
"failure_added": True,
}
def test_ProxyConfig__add_callbacks_from_db_config_bad_config_raises():
pc = ProxyConfig()
with pytest.raises(AttributeError):
# Non-dict input — .get will fail.
pc._add_callbacks_from_db_config(None) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# ProxyConfig._encrypt_env_variables
# ---------------------------------------------------------------------------
def test_ProxyConfig__encrypt_env_variables_returns_dict(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.encrypt_value_helper",
lambda value, new_encryption_key=None: f"ENC[{value}]",
)
pc = ProxyConfig()
out = pc._encrypt_env_variables({"A": "1", "B": "2", "C": "3"})
assert out == {"A": "ENC[1]", "B": "ENC[2]", "C": "ENC[3]"}
def test_ProxyConfig__encrypt_env_variables_invalid_raises():
pc = ProxyConfig()
with pytest.raises(AttributeError):
# Non-dict input — .items() fails.
pc._encrypt_env_variables(None) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# ProxyConfig._decrypt_and_set_db_env_variables
# ---------------------------------------------------------------------------
def test_ProxyConfig__decrypt_and_set_db_env_variables_sets_env(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.decrypt_value_helper",
lambda value, key, return_original_value=False: value + "-dec",
)
monkeypatch.delenv("KEY_X", raising=False)
monkeypatch.delenv("KEY_Y", raising=False)
pc = ProxyConfig()
out = pc._decrypt_and_set_db_env_variables({"KEY_X": "x", "KEY_Y": "y"})
snapshot = {
"KEY_X_env": os.environ.get("KEY_X"),
"KEY_Y_env": os.environ.get("KEY_Y"),
"returned_keys": sorted(out.keys()),
}
assert snapshot == {
"KEY_X_env": "x-dec",
"KEY_Y_env": "y-dec",
"returned_keys": ["KEY_X", "KEY_Y"],
}
def test_ProxyConfig__decrypt_and_set_db_env_variables_invalid_dict_raises():
pc = ProxyConfig()
with pytest.raises(AttributeError):
pc._decrypt_and_set_db_env_variables("not-a-dict") # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# ProxyConfig._decrypt_db_variables
# ---------------------------------------------------------------------------
def test_ProxyConfig__decrypt_db_variables_returns_decrypted(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.decrypt_value_helper",
lambda value, key, return_original_value: f"D({value})",
)
pc = ProxyConfig()
out = pc._decrypt_db_variables({"a": "1", "b": "2", "c": "3"})
assert out == {"a": "D(1)", "b": "D(2)", "c": "D(3)"}
def test_ProxyConfig__decrypt_db_variables_invalid_raises():
pc = ProxyConfig()
with pytest.raises(AttributeError):
pc._decrypt_db_variables(None) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# ProxyConfig._encrypt_env_variables_for_db
# ---------------------------------------------------------------------------
def test_ProxyConfig__encrypt_env_variables_for_db_idempotent(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.decrypt_value_helper",
lambda value, key, return_original_value: value,
)
monkeypatch.setattr(
"litellm.proxy.proxy_server.encrypt_value_helper",
lambda value, new_encryption_key=None: f"ENC[{value}]",
)
pc = ProxyConfig()
out = pc._encrypt_env_variables_for_db({"A": "1", "B": "2", "C": "3"})
assert out == {"A": "ENC[1]", "B": "ENC[2]", "C": "ENC[3]"}
def test_ProxyConfig__encrypt_env_variables_for_db_invalid_raises():
pc = ProxyConfig()
with pytest.raises(AttributeError):
pc._encrypt_env_variables_for_db(None) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# ProxyConfig._parse_router_settings_value
# ---------------------------------------------------------------------------
def test_ProxyConfig__parse_router_settings_value_handles_inputs():
result = {
"dict": ProxyConfig._parse_router_settings_value({"a": 1}),
"yaml_string": ProxyConfig._parse_router_settings_value("a: 1\nb: 2"),
"none": ProxyConfig._parse_router_settings_value(None),
}
assert result == {
"dict": {"a": 1},
"yaml_string": {"a": 1, "b": 2},
"none": None,
}
def test_ProxyConfig__parse_router_settings_value_invalid_returns_none():
# Non-dict, non-parseable scalar -> None.
assert ProxyConfig._parse_router_settings_value(12345) is None
# Empty dict -> None (not truthy).
assert ProxyConfig._parse_router_settings_value({}) is None
# ---------------------------------------------------------------------------
# ProxyConfig._get_hierarchical_router_settings
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__get_hierarchical_router_settings_key_wins():
pc = ProxyConfig()
fake_key = SimpleNamespace(
router_settings={"timeout": 30, "retries": 2, "model": "gpt-4"},
team_id=None,
)
out = await pc._get_hierarchical_router_settings(
user_api_key_dict=fake_key,
prisma_client=None,
proxy_logging_obj=None,
)
assert out == {"timeout": 30, "retries": 2, "model": "gpt-4"}
@pytest.mark.asyncio
async def test_ProxyConfig__get_hierarchical_router_settings_missing_returns_none():
pc = ProxyConfig()
fake_key = SimpleNamespace(router_settings=None, team_id=None)
out = await pc._get_hierarchical_router_settings(
user_api_key_dict=fake_key,
prisma_client=None,
proxy_logging_obj=None,
)
assert out is None
# ---------------------------------------------------------------------------
# ProxyConfig._add_router_settings_from_db_config
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__add_router_settings_from_db_config_updates_router():
pc = ProxyConfig()
fake_router = MagicMock()
fake_router.update_settings = MagicMock()
fake_prisma = MagicMock()
fake_prisma.db.litellm_config.find_first = AsyncMock(
return_value=SimpleNamespace(
param_value={"timeout": 30, "retries": 2, "fallbacks": []}
)
)
config_data = {"router_settings": {"timeout": 10}}
await pc._add_router_settings_from_db_config(
config_data=config_data,
llm_router=fake_router,
prisma_client=fake_prisma,
)
snapshot = {
"called": fake_router.update_settings.called,
"call_count": fake_router.update_settings.call_count,
"kwargs_keys": sorted(
list(fake_router.update_settings.call_args.kwargs.keys())
),
}
assert snapshot == {
"called": True,
"call_count": 1,
"kwargs_keys": ["fallbacks", "retries", "timeout"],
}
@pytest.mark.asyncio
async def test_ProxyConfig__add_router_settings_from_db_config_none_router_noop():
pc = ProxyConfig()
# No router and no prisma — should silently return.
await pc._add_router_settings_from_db_config(
config_data={}, llm_router=None, prisma_client=None
)
# Error-style: bad call signature raises.
with pytest.raises(TypeError):
await pc._add_router_settings_from_db_config() # type: ignore[call-arg]
# ---------------------------------------------------------------------------
# ProxyConfig._add_general_settings_from_db_config
# ---------------------------------------------------------------------------
def test_ProxyConfig__add_general_settings_from_db_config_merges_alerting():
pc = ProxyConfig()
proxy_logging = MagicMock()
general = {"alerting": ["slack"]}
config_data = {"general_settings": {"alerting": ["email", "slack"]}}
pc._add_general_settings_from_db_config(
config_data=config_data,
general_settings=general,
proxy_logging_obj=proxy_logging,
)
snapshot = {
"alerting": sorted(general["alerting"]),
"logging_called": proxy_logging.update_values.called,
"merged_count": len(general["alerting"]),
}
assert snapshot == {
"alerting": ["email", "slack"],
"logging_called": True,
"merged_count": 2,
}
def test_ProxyConfig__add_general_settings_from_db_config_bad_config_raises():
pc = ProxyConfig()
with pytest.raises(AttributeError):
pc._add_general_settings_from_db_config(
config_data=None, # type: ignore[arg-type]
general_settings={},
proxy_logging_obj=MagicMock(),
)
# ---------------------------------------------------------------------------
# ProxyConfig._reschedule_spend_log_cleanup_job
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__reschedule_spend_log_cleanup_job_no_scheduler(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.scheduler", None)
pc = ProxyConfig()
try:
await pc._reschedule_spend_log_cleanup_job()
raised = False
except Exception:
raised = True
snapshot = {"raised": raised, "called": True, "scheduler_was": "none"}
assert snapshot == {"raised": False, "called": True, "scheduler_was": "none"}
@pytest.mark.asyncio
async def test_ProxyConfig__reschedule_spend_log_cleanup_job_invalid_cron(monkeypatch):
fake_scheduler = MagicMock()
fake_scheduler.remove_job = MagicMock()
fake_scheduler.add_job = MagicMock()
monkeypatch.setattr("litellm.proxy.proxy_server.scheduler", fake_scheduler)
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{
"maximum_spend_logs_retention_period": "1d",
"maximum_spend_logs_cleanup_cron": "INVALID CRON STRING",
},
)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
pc = ProxyConfig()
# Invalid cron is caught and logged — does not raise outward.
await pc._reschedule_spend_log_cleanup_job()
# But add_job should not have been called for the invalid cron path.
assert fake_scheduler.add_job.call_count == 0
# ---------------------------------------------------------------------------
# ProxyConfig._update_general_settings
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_ProxyConfig__update_general_settings_updates_max_parallel(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{},
)
pc = ProxyConfig()
await pc._update_general_settings(
{
"max_parallel_requests": 7,
"global_max_parallel_requests": 99,
"ui_access_mode": "admin_only",
}
)
from litellm.proxy import proxy_server as ps
snapshot = {
"max_parallel_requests": ps.general_settings.get("max_parallel_requests"),
"global_max_parallel_requests": ps.general_settings.get(
"global_max_parallel_requests"
),
"ui_access_mode": ps.general_settings.get("ui_access_mode"),
}
assert snapshot == {
"max_parallel_requests": 7,
"global_max_parallel_requests": 99,
"ui_access_mode": "admin_only",
}
@pytest.mark.asyncio
async def test_ProxyConfig__update_general_settings_none_input_noop():
pc = ProxyConfig()
# None input returns early.
result = await pc._update_general_settings(db_general_settings=None)
assert result is None
# Error-style: dict() will fail on non-mapping non-None input.
with pytest.raises(Exception):
await pc._update_general_settings(db_general_settings=12345) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# ProxyConfig._update_config_fields
# ---------------------------------------------------------------------------
def test_ProxyConfig__update_config_fields_merges_dict():
pc = ProxyConfig()
current = {"general_settings": {"a": 1, "b": 2}}
out = pc._update_config_fields(
current_config=current,
param_name="general_settings",
db_param_value={"b": 3, "c": 4, "d": 5},
)
assert out == {"general_settings": {"a": 1, "b": 3, "c": 4, "d": 5}}
def test_ProxyConfig__update_config_fields_invalid_param_raises():
pc = ProxyConfig()
with pytest.raises(Exception):
# Missing required arg.
pc._update_config_fields(current_config={}, param_name="general_settings") # type: ignore[call-arg]