Files
litellm/tests/test_litellm/proxy/db/test_db_url_settings.py
T
95e3d136e1 test(google): add google-genai SDK proxy integration tests (#29781)
* test(google): add google-genai SDK proxy integration tests for Gemini and Vertex

Pin google-genai in the CI dependency group and exercise streaming/non-streaming
generate_content through the LiteLLM proxy in the existing unified_google_tests suite.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(test): address Greptile review for google-genai proxy SDK tests

Restore GOOGLE_APPLICATION_CREDENTIALS after the module proxy fixture tears down,
initialize temp-file tracking on the proxy SDK base class, and skip litellm reload
for proxy_genai_sdk tests so the module-scoped proxy server stays consistent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(test): only load Vertex credentials when keys exist for proxy SDK tests

Avoid writing empty GOOGLE_APPLICATION_CREDENTIALS temp files so Vertex tests
skip cleanly without credentials, use a session-scoped proxy fixture, and clean up
per-test credential temp files.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(test): scope google-genai pin to unified_google_tests only

Remove google-genai from the ci dependency group and pin it in
tests/unified_google_tests/requirements.txt for local test installs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(google): tie litellm reload skip to proxy fixture dependency

Replace the name-based reload guard with a check on whether the test
requests the google_genai_proxy_url fixture, so the skip stays correct
if the proxy SDK tests are renamed.

* fix(test): stop DatabaseURLSettings tests leaking DATABASE_URL into os.environ

The autouse env scrubber relied on monkeypatch.delenv, but apply_to_env
writes DATABASE_URL straight into os.environ, which monkeypatch never
tracks and therefore never undoes. The synthesized writer.example.com URL
leaked past the last test in this module and into proxy-infra tests that
read DATABASE_URL to decide whether to hit a real database, e.g.
test_deprecated_key_grace_period_cache_hit_path, turning an intended skip
into a ConnectError. Snapshot and restore the managed vars directly so the
original environment is reinstated regardless of how it was mutated.

* test(google): drop redundant per-test vertex credential setup

The session-scoped google_genai_proxy_url fixture already configures
GOOGLE_APPLICATION_CREDENTIALS before the proxy starts, and
_require_proxy_sdk skips when credentials are missing, so the per-test
_setup_vertex_credentials_if_needed helper and its temp-file tracking
never did any work. Remove it to keep the ABC self-contained.

* test(google): declare model_config contract on proxy SDK ABC

_skip_reason_if_credentials_missing reads self.model_config to pick the
provider, but that property was only declared on the sibling
BaseGoogleGenAITest. Make the dependency explicit by adding model_config
as an abstract property on BaseGoogleGenAIProxySDKTest so the ABC is
self-contained and a standalone subclass fails fast instead of hitting an
AttributeError.

* test(google): narrow streaming error catch to Exception

Catching BaseException in the streaming assertion swallowed
KeyboardInterrupt and SystemExit, turning a Ctrl-C into a test failure
message instead of letting pytest interrupt cleanly. Only genuine runtime
errors should be recorded as stream failures, so catch Exception.

* test(google): initialize proxy on the same loop that serves it

The proxy was initialized via asyncio.run() on the main thread, which
creates and tears down a throwaway event loop, while requests were served
on a separate loop in the worker thread. Any asyncio primitive bound to
the init loop would be unusable once serving started. Run initialize()
on the worker thread's loop right before server.serve() so setup and
request handling share a single event loop.

* test(google): drop redundant google-genai requirements pin

google-genai>=1.37.0,<2.0 is already declared in the proxy-runtime extra,
which the google_generate_content_endpoint_testing CI job installs via
uv sync --all-extras. The standalone tests/unified_google_tests/requirements.txt
duplicated that pin with a narrower ==1.37.0 specifier and was never
installed by CI, so it added a second source of truth without changing
what gets installed. Drop it and rely on the proxy-runtime extra.

* chore: revert incidental uv.lock exclude-newer bump

The google-genai ci pin was added and then dropped (it is already
provided by the proxy-runtime group), but each uv lock recomputed the
relative exclude-newer span, leaving only a timestamp bump in uv.lock.
Restore it to the base value so this test-only PR carries no lockfile
change.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-05 21:05:32 +00:00

290 lines
9.9 KiB
Python

"""Tests for ``DatabaseURLSettings``.
The model assembles ``DATABASE_URL`` (and optionally
``DATABASE_URL_READ_REPLICA``) from the discrete ``DATABASE_*`` env vars
emitted by the ``helm/litellm`` chart, before Prisma initializes. It covers
both IAM auth (mint a short-lived token) and password auth, for both the
writer and the read replica.
The reader URL is opt-in via ``DATABASE_HOST_READ_REPLICA`` and must not
clobber a pre-existing ``DATABASE_URL_READ_REPLICA``. A pre-existing
``DATABASE_URL`` (password auth) is likewise left untouched.
"""
import os
from unittest.mock import patch
import pytest
from litellm.proxy.db.db_url_settings import DatabaseURLSettings
def _apply() -> bool:
"""Run the production call path: load from env, write to env."""
return DatabaseURLSettings.from_env().apply_to_env()
_MANAGED_DB_ENV_VARS = (
"IAM_TOKEN_DB_AUTH",
"DATABASE_URL",
"DATABASE_URL_READ_REPLICA",
"DATABASE_HOST",
"DATABASE_PORT",
"DATABASE_USER",
"DATABASE_USERNAME",
"DATABASE_NAME",
"DATABASE_SCHEMA",
"DATABASE_PASSWORD",
"DATABASE_HOST_READ_REPLICA",
"DATABASE_PORT_READ_REPLICA",
"DATABASE_USER_READ_REPLICA",
"DATABASE_USERNAME_READ_REPLICA",
"DATABASE_NAME_READ_REPLICA",
"DATABASE_SCHEMA_READ_REPLICA",
"DATABASE_PASSWORD_READ_REPLICA",
)
@pytest.fixture(autouse=True)
def _scrub_db_env():
"""Start each test from a clean slate and restore the original env afterward.
``apply_to_env`` writes ``DATABASE_URL`` straight into ``os.environ``, which
``monkeypatch`` cannot undo. Snapshotting and restoring here keeps a
synthesized URL (e.g. ``writer.example.com``) from leaking into later tests
that read ``DATABASE_URL`` to decide whether to hit a real database.
"""
saved = {var: os.environ.get(var) for var in _MANAGED_DB_ENV_VARS}
for var in _MANAGED_DB_ENV_VARS:
os.environ.pop(var, None)
try:
yield
finally:
for var, value in saved.items():
if value is None:
os.environ.pop(var, None)
else:
os.environ[var] = value
def _stub_iam_token(token: str = "FAKE_TOKEN"):
"""Patch the AWS-touching token mint so tests don't need boto3 / network."""
return patch(
"litellm.proxy.auth.rds_iam_token.generate_iam_auth_token",
return_value=token,
)
# ---------------------------------------------------------------------------
# IAM auth
# ---------------------------------------------------------------------------
def test_returns_false_when_nothing_configured(monkeypatch):
"""No env mutation, no error — just a False return."""
assert _apply() is False
assert "DATABASE_URL" not in os.environ
def test_assembles_writer_url_when_iam_enabled(monkeypatch):
monkeypatch.setenv("IAM_TOKEN_DB_AUTH", "true")
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
with _stub_iam_token("WRITER_TOKEN"):
assert _apply() is True
assert (
os.environ["DATABASE_URL"]
== "postgresql://litellm:WRITER_TOKEN@writer.example.com:5432/litellm_db"
)
# Reader was never configured, so it must not have been set.
assert "DATABASE_URL_READ_REPLICA" not in os.environ
def test_missing_writer_envs_raises(monkeypatch):
monkeypatch.setenv("IAM_TOKEN_DB_AUTH", "true")
# DATABASE_HOST intentionally unset.
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
with pytest.raises(RuntimeError, match="DATABASE_HOST"):
_apply()
def test_reader_url_assembled_when_host_set_and_url_unset(monkeypatch):
monkeypatch.setenv("IAM_TOKEN_DB_AUTH", "true")
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_HOST_READ_REPLICA", "reader.example.com")
with _stub_iam_token("READER_TOKEN"):
_apply()
assert (
os.environ["DATABASE_URL_READ_REPLICA"]
== "postgresql://litellm:READER_TOKEN@reader.example.com:5432/litellm_db"
)
def test_reader_url_not_clobbered_when_already_set(monkeypatch):
"""If the operator pinned DATABASE_URL_READ_REPLICA (e.g. a non-IAM
reader), the model must leave it untouched even though
DATABASE_HOST_READ_REPLICA is also set."""
monkeypatch.setenv("IAM_TOKEN_DB_AUTH", "true")
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_HOST_READ_REPLICA", "reader.example.com")
monkeypatch.setenv(
"DATABASE_URL_READ_REPLICA",
"postgresql://app:secret@reader.example.com:5432/litellm_db",
)
with _stub_iam_token("READER_TOKEN"):
_apply()
assert (
os.environ["DATABASE_URL_READ_REPLICA"]
== "postgresql://app:secret@reader.example.com:5432/litellm_db"
)
def test_reader_url_skipped_when_host_unset(monkeypatch):
monkeypatch.setenv("IAM_TOKEN_DB_AUTH", "true")
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
with _stub_iam_token("WRITER_TOKEN"):
_apply()
assert "DATABASE_URL_READ_REPLICA" not in os.environ
def test_reader_field_fallbacks_default_to_writer_values(monkeypatch):
"""When *_READ_REPLICA fields are unset (other than host), they fall
back to the writer's user / name / schema."""
monkeypatch.setenv("IAM_TOKEN_DB_AUTH", "true")
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_SCHEMA", "public")
monkeypatch.setenv("DATABASE_HOST_READ_REPLICA", "reader.example.com")
with _stub_iam_token("READER_TOKEN"):
_apply()
assert (
os.environ["DATABASE_URL_READ_REPLICA"]
== "postgresql://litellm:READER_TOKEN@reader.example.com:5432/litellm_db?schema=public"
)
# ---------------------------------------------------------------------------
# Password auth
# ---------------------------------------------------------------------------
def test_assembles_writer_url_from_password(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_PASSWORD", "s3cr3t")
assert _apply() is True
assert (
os.environ["DATABASE_URL"]
== "postgresql://litellm:s3cr3t@writer.example.com:5432/litellm_db"
)
def test_writer_password_is_percent_encoded(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_PASSWORD", "p@ss/w:rd")
assert _apply() is True
assert (
os.environ["DATABASE_URL"]
== "postgresql://litellm:p%40ss%2Fw%3Ard@writer.example.com:5432/litellm_db"
)
def test_writer_url_not_clobbered_when_already_set(monkeypatch):
"""An operator-pinned DATABASE_URL (e.g. helm's $(VAR) assembly) always
wins over the discrete fields."""
monkeypatch.setenv(
"DATABASE_URL", "postgresql://pinned:url@db.example.com:5432/litellm_db"
)
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_PASSWORD", "s3cr3t")
assert _apply() is False
assert (
os.environ["DATABASE_URL"]
== "postgresql://pinned:url@db.example.com:5432/litellm_db"
)
def test_writer_url_passwordless(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
assert _apply() is True
assert (
os.environ["DATABASE_URL"]
== "postgresql://litellm@writer.example.com:5432/litellm_db"
)
def test_database_username_alias(monkeypatch):
"""DATABASE_USERNAME is accepted as an alias for DATABASE_USER (parity
with construct_database_url_from_env_vars)."""
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USERNAME", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_PASSWORD", "s3cr3t")
assert _apply() is True
assert (
os.environ["DATABASE_URL"]
== "postgresql://litellm:s3cr3t@writer.example.com:5432/litellm_db"
)
def test_password_reader_falls_back_to_writer_password(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_PASSWORD", "s3cr3t")
monkeypatch.setenv("DATABASE_HOST_READ_REPLICA", "reader.example.com")
assert _apply() is True
assert (
os.environ["DATABASE_URL_READ_REPLICA"]
== "postgresql://litellm:s3cr3t@reader.example.com:5432/litellm_db"
)
def test_password_reader_uses_own_credentials(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "writer.example.com")
monkeypatch.setenv("DATABASE_USER", "litellm")
monkeypatch.setenv("DATABASE_NAME", "litellm_db")
monkeypatch.setenv("DATABASE_PASSWORD", "s3cr3t")
monkeypatch.setenv("DATABASE_HOST_READ_REPLICA", "reader.example.com")
monkeypatch.setenv("DATABASE_USER_READ_REPLICA", "litellm_ro")
monkeypatch.setenv("DATABASE_PASSWORD_READ_REPLICA", "ro_pw")
assert _apply() is True
assert (
os.environ["DATABASE_URL_READ_REPLICA"]
== "postgresql://litellm_ro:ro_pw@reader.example.com:5432/litellm_db"
)