mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-12 21:04:10 +00:00
1aed5e1bbd
* 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>
202 lines
6.3 KiB
Python
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
|