Files
litellm/tests/test_litellm/proxy/utils/helpers/test_misc_helpers.py
T
yuneng-jiang 1aed5e1bbd test(proxy/utils): pin bottom-of-file helper behavior (#29509)
* test(proxy/utils): pin bottom-of-file helper behavior

Pin current behavior of the bottom-of-file pure-function helpers in
litellm/proxy/utils.py (projection, team config, time helpers,
guardrail merge, error helpers, URL/path helpers, premium gate, model
access, and misc DB/API-key helpers).

Adds tests/test_litellm/proxy/utils/helpers/ with one happy + one error
test per pinned symbol; folds the prior single-test
tests/test_litellm/proxy/test_utils.py into test_url_helpers.py and
deletes the old file. _pin_check.py and _coverage_check.py serve as
local stopping gates.

Adds tests/test_litellm/proxy/utils to the existing test-path block in
.github/workflows/test-unit-proxy-endpoints.yml.

Plan:     https://www.notion.so/37343b8acdab81f68f39f66915f62bcf
Pin list: https://www.notion.so/37343b8acdab8150acdbf40e5756869f

* test(proxy/utils): apply greptile fixes to behavior-pinning gates

Address findings from the sibling PR1/PR2 greptile reviews that also
apply to this PR:

- Commit pin_list.txt alongside the gate script (was previously a
  gitignored .pin_list.txt fetched from Notion). The gate is now
  reproducible without out-of-band setup.
- Resolve the coverage region by locating the first pinned symbol's
  def line in litellm/proxy/utils.py at runtime, instead of hardcoded
  line numbers that drift when lines above shift.
- Word-boundary the pin reference check so pins like update_spend do
  not falsely match update_spend_logs_job.
- Drop the dead _harness_smoke_test.py exclusion; the test_*.py glob
  already filters underscore-prefixed files.

* test(proxy/utils): drop local-only stopping-signal scripts

Remove _pin_check.py, _coverage_check.py, and pin_list.txt. These were
dev-time tooling for knowing when test authoring was done; they are
not wired into CI and the test files themselves are the merge artifact.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-02 17:45:19 -07:00

202 lines
6.3 KiB
Python

import pytest
from fastapi import HTTPException
from litellm.proxy.utils import (
construct_database_url_from_env_vars,
get_prisma_client_or_throw,
is_valid_api_key,
)
def normalize(value):
return value
def test_get_prisma_client_or_throw_happy_path_returns_client(monkeypatch):
sentinel = object()
import litellm.proxy.proxy_server as ps
monkeypatch.setattr(ps, "prisma_client", sentinel, raising=False)
result = get_prisma_client_or_throw("some message")
summary = {
"is_sentinel": result is sentinel,
"message_arg": "some message",
"raised": False,
}
assert summary == {
"is_sentinel": True,
"message_arg": "some message",
"raised": False,
}
def test_get_prisma_client_or_throw_raises_when_client_none(monkeypatch):
import litellm.proxy.proxy_server as ps
monkeypatch.setattr(ps, "prisma_client", None, raising=False)
with pytest.raises(HTTPException) as exc_info:
get_prisma_client_or_throw("db not connected")
snapshot = {
"status_code": exc_info.value.status_code,
"is_dict_detail": isinstance(exc_info.value.detail, dict),
"error_message": exc_info.value.detail["error"],
}
assert snapshot == {
"status_code": 500,
"is_dict_detail": True,
"error_message": "db not connected",
}
def test_is_valid_api_key_happy_path_sk_prefix():
summary = {
"result": is_valid_api_key("sk-abc123_XYZ-456"),
"key": "sk-abc123_XYZ-456",
"len": len("sk-abc123_XYZ-456"),
}
assert summary == {
"result": True,
"key": "sk-abc123_XYZ-456",
"len": 17,
}
def test_is_valid_api_key_happy_path_hashed_64_hex():
key = "a" * 64
summary = {
"result": is_valid_api_key(key),
"key_len": len(key),
"is_hex": True,
}
assert summary == {
"result": True,
"key_len": 64,
"is_hex": True,
}
def test_is_valid_api_key_happy_path_mixed_case_hex():
key = "AbCdEf0123456789" * 4
summary = {
"result": is_valid_api_key(key),
"key_len": len(key),
"first": key[0],
}
assert summary == {
"result": True,
"key_len": 64,
"first": "A",
}
def test_is_valid_api_key_error_path_too_long():
assert is_valid_api_key("sk-" + "a" * 200) is False
def test_is_valid_api_key_error_path_non_string():
assert is_valid_api_key(12345) is False # type: ignore[arg-type]
def test_is_valid_api_key_error_path_invalid_format():
assert is_valid_api_key("not-a-valid-key-format!!!!") is False
def test_is_valid_api_key_error_path_too_short():
assert is_valid_api_key("sk") is False
def test_construct_database_url_from_env_vars_happy_path_full(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "db.example.com")
monkeypatch.setenv("DATABASE_USERNAME", "user")
monkeypatch.setenv("DATABASE_PASSWORD", "pass")
monkeypatch.setenv("DATABASE_NAME", "litellm")
monkeypatch.delenv("DATABASE_SCHEMA", raising=False)
result = construct_database_url_from_env_vars()
summary = {
"result": result,
"host": "db.example.com",
"scheme": result.split("://", 1)[0] if result else None,
"has_password": "pass" in (result or ""),
}
assert summary == {
"result": "postgresql://user:pass@db.example.com/litellm",
"host": "db.example.com",
"scheme": "postgresql",
"has_password": True,
}
def test_construct_database_url_from_env_vars_happy_path_no_password(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "db.example.com")
monkeypatch.setenv("DATABASE_USERNAME", "user")
monkeypatch.delenv("DATABASE_PASSWORD", raising=False)
monkeypatch.setenv("DATABASE_NAME", "litellm")
monkeypatch.delenv("DATABASE_SCHEMA", raising=False)
result = construct_database_url_from_env_vars()
summary = {
"result": result,
"no_colon_password": ":pass@" not in (result or ""),
"host": "db.example.com",
"user": "user",
}
assert summary == {
"result": "postgresql://user@db.example.com/litellm",
"no_colon_password": True,
"host": "db.example.com",
"user": "user",
}
def test_construct_database_url_from_env_vars_special_chars_encoded(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "db.example.com")
monkeypatch.setenv("DATABASE_USERNAME", "us er@x")
monkeypatch.setenv("DATABASE_PASSWORD", "p@ss/word")
monkeypatch.setenv("DATABASE_NAME", "lite/llm")
monkeypatch.delenv("DATABASE_SCHEMA", raising=False)
result = construct_database_url_from_env_vars()
summary = {
"result": result,
"username_encoded": "us+er%40x" in result,
"password_encoded": "p%40ss%2Fword" in result,
"name_encoded": "lite%2Fllm" in result,
}
assert summary == {
"result": "postgresql://us+er%40x:p%40ss%2Fword@db.example.com/lite%2Fllm",
"username_encoded": True,
"password_encoded": True,
"name_encoded": True,
}
def test_construct_database_url_from_env_vars_with_schema(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "db.example.com")
monkeypatch.setenv("DATABASE_USERNAME", "user")
monkeypatch.setenv("DATABASE_PASSWORD", "pass")
monkeypatch.setenv("DATABASE_NAME", "litellm")
monkeypatch.setenv("DATABASE_SCHEMA", "public")
result = construct_database_url_from_env_vars()
summary = {
"result": result,
"schema_appended": result.endswith("?schema=public"),
"host": "db.example.com",
}
assert summary == {
"result": "postgresql://user:pass@db.example.com/litellm?schema=public",
"schema_appended": True,
"host": "db.example.com",
}
def test_construct_database_url_from_env_vars_error_path_missing_host(monkeypatch):
monkeypatch.delenv("DATABASE_HOST", raising=False)
monkeypatch.setenv("DATABASE_USERNAME", "user")
monkeypatch.setenv("DATABASE_NAME", "litellm")
assert construct_database_url_from_env_vars() is None
def test_construct_database_url_from_env_vars_error_path_missing_username(monkeypatch):
monkeypatch.setenv("DATABASE_HOST", "db.example.com")
monkeypatch.delenv("DATABASE_USERNAME", raising=False)
monkeypatch.setenv("DATABASE_NAME", "litellm")
assert construct_database_url_from_env_vars() is None