mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* test: run the 30 test files stranded in the second mirror
tests/litellm sat beside tests/test_litellm, which is the mirror the repo
convention names, and no job collected it. The allowlist called the directory
unresolved and assumed it was a duplicate. It is not: 30 of its 34 files have no
counterpart in the real mirror, so they are tests nobody has run since they were
written, not copies of tests that run elsewhere.
Moving them in is byte-identical, and it is what makes them run. Every one is
now claimed by a shard's test-path rather than by an allowlist entry, and the
216 tests they hold pass. Directories that needed to become packages did, since
several files are named test_transformation.py and pytest cannot import two of
those from non-package directories in one session.
Never running is why three assertions had drifted away from the code:
* nvidia.nemotron-super-3-120b max_output_tokens, 32000 -> 32768
* sambanova/MiniMax-M2.7 max_input_tokens, 204800 -> 196608
* the Vertex text-to-speech handler moved from data= to json=, so the test
reads the decoded body off the json kwarg instead of parsing the data one
The first two follow model_prices_and_context_window.json, which the catalog
sync keeps current; the third follows the handler. In all three the test was the
stale side.
The lint workflow ran test_no_hardcoded_secrets.py by path and now points at the
new one.
Four files stay behind. Each shares a filename with a live test whose contents
are disjoint from it, so landing those means merging test bodies, which is a
content review rather than a move. The allowlist entry now names those four and
records how many tests each would bring, in place of calling the whole
directory unresolved.
* fix(ci): keep the secret scan out of the mirror's conftest
The secret-scan job runs pytest under uv run --no-project, so its environment
holds pytest and nothing else. That worked while the file sat in tests/litellm,
which has no conftest, and broke the moment it moved into tests/test_litellm,
whose conftest imports litellm on collection: ModuleNotFoundError: No module
named 'dotenv', before a single test ran.
The file is a repo-wide static scan that imports only base64, os, re and pytest,
so it belongs with the other repo-wide checks in tests/code_coverage_tests,
which has no conftest, rather than in the package mirror. Installing the full
dependency set into a 15-second job to satisfy a conftest it does not use would
be the wrong trade.
Verified with the job's exact command:
uv run --no-project --with 'pytest==9.0.2' pytest \
tests/code_coverage_tests/test_no_hardcoded_secrets.py -q
1 passed in 0.47s
204 lines
6.9 KiB
Python
204 lines
6.9 KiB
Python
"""
|
|
Unit tests for litellm.proxy_auth module.
|
|
|
|
Tests the OAuth2/JWT token management for LiteLLM Proxy authentication.
|
|
"""
|
|
|
|
import time
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from litellm.proxy_auth import (
|
|
AccessToken,
|
|
AzureADCredential,
|
|
GenericOAuth2Credential,
|
|
ProxyAuthHandler,
|
|
)
|
|
|
|
|
|
class TestAccessToken:
|
|
"""Tests for AccessToken dataclass."""
|
|
|
|
def test_access_token_creation(self):
|
|
"""Test AccessToken can be created with required fields."""
|
|
token = AccessToken(token="test-token", expires_on=1234567890)
|
|
assert token.token == "test-token"
|
|
assert token.expires_on == 1234567890
|
|
|
|
def test_access_token_equality(self):
|
|
"""Test AccessToken equality comparison."""
|
|
token1 = AccessToken(token="test", expires_on=123)
|
|
token2 = AccessToken(token="test", expires_on=123)
|
|
assert token1 == token2
|
|
|
|
|
|
class MockCredential:
|
|
"""Mock credential for testing."""
|
|
|
|
def __init__(self, expires_in_seconds: int = 3600):
|
|
self.call_count = 0
|
|
self.expires_in = expires_in_seconds
|
|
|
|
def get_token(self, scope: str) -> AccessToken:
|
|
self.call_count += 1
|
|
return AccessToken(
|
|
token=f"mock-token-{self.call_count}",
|
|
expires_on=int(time.time()) + self.expires_in,
|
|
)
|
|
|
|
|
|
class TestProxyAuthHandler:
|
|
"""Tests for ProxyAuthHandler."""
|
|
|
|
def test_get_auth_headers_returns_bearer_token(self):
|
|
"""Test that get_auth_headers returns correct Authorization header."""
|
|
cred = MockCredential()
|
|
handler = ProxyAuthHandler(credential=cred, scope="test-scope")
|
|
|
|
headers = handler.get_auth_headers()
|
|
|
|
assert "Authorization" in headers
|
|
assert headers["Authorization"].startswith("Bearer ")
|
|
assert "mock-token-1" in headers["Authorization"]
|
|
|
|
def test_token_caching(self):
|
|
"""Test that tokens are cached and not re-requested."""
|
|
cred = MockCredential(expires_in_seconds=3600) # Long expiry
|
|
handler = ProxyAuthHandler(credential=cred, scope="test-scope")
|
|
|
|
# Multiple calls should only request token once
|
|
handler.get_auth_headers()
|
|
handler.get_auth_headers()
|
|
handler.get_auth_headers()
|
|
|
|
assert cred.call_count == 1
|
|
|
|
def test_token_refresh_when_about_to_expire(self):
|
|
"""Test that tokens are refreshed when about to expire (within 60s buffer)."""
|
|
cred = MockCredential(expires_in_seconds=30) # Expires in 30s (< 60s buffer)
|
|
handler = ProxyAuthHandler(credential=cred, scope="test-scope")
|
|
|
|
# First call gets token
|
|
handler.get_auth_headers()
|
|
# Second call should refresh because token expires within 60s buffer
|
|
handler.get_auth_headers()
|
|
|
|
assert cred.call_count == 2
|
|
|
|
def test_get_token_method(self):
|
|
"""Test the get_token method returns AccessToken."""
|
|
cred = MockCredential()
|
|
handler = ProxyAuthHandler(credential=cred, scope="test-scope")
|
|
|
|
token = handler.get_token()
|
|
|
|
assert isinstance(token, AccessToken)
|
|
assert token.token == "mock-token-1"
|
|
|
|
|
|
class TestAzureADCredential:
|
|
"""Tests for AzureADCredential."""
|
|
|
|
def test_lazy_initialization(self):
|
|
"""Test that azure-identity is not imported until get_token is called."""
|
|
# This should not raise ImportError even if azure-identity is not installed
|
|
cred = AzureADCredential(credential=None)
|
|
# _initialized should be False until get_token is called
|
|
assert cred._initialized is False
|
|
|
|
def test_wraps_azure_credential(self):
|
|
"""Test that AzureADCredential wraps an azure-identity credential."""
|
|
# Mock Azure credential
|
|
mock_azure_cred = Mock()
|
|
mock_azure_cred.get_token.return_value = Mock(
|
|
token="azure-token", expires_on=9999999999
|
|
)
|
|
|
|
cred = AzureADCredential(credential=mock_azure_cred)
|
|
token = cred.get_token("https://graph.microsoft.com/.default")
|
|
|
|
assert token.token == "azure-token"
|
|
assert token.expires_on == 9999999999
|
|
mock_azure_cred.get_token.assert_called_once_with(
|
|
"https://graph.microsoft.com/.default"
|
|
)
|
|
|
|
|
|
class TestGenericOAuth2Credential:
|
|
"""Tests for GenericOAuth2Credential."""
|
|
|
|
def test_token_request(self):
|
|
"""Test that GenericOAuth2Credential makes correct OAuth2 request."""
|
|
with patch("httpx.post") as mock_post:
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "oauth2-token",
|
|
"expires_in": 3600,
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
mock_post.return_value = mock_response
|
|
|
|
cred = GenericOAuth2Credential(
|
|
client_id="test-client",
|
|
client_secret="test-secret",
|
|
token_url="https://example.com/oauth2/token",
|
|
)
|
|
token = cred.get_token("test-scope")
|
|
|
|
assert token.token == "oauth2-token"
|
|
mock_post.assert_called_once()
|
|
call_kwargs = mock_post.call_args
|
|
assert call_kwargs[1]["data"]["grant_type"] == "client_credentials"
|
|
assert call_kwargs[1]["data"]["client_id"] == "test-client"
|
|
assert call_kwargs[1]["data"]["client_secret"] == "test-secret"
|
|
assert call_kwargs[1]["data"]["scope"] == "test-scope"
|
|
|
|
def test_token_caching(self):
|
|
"""Test that GenericOAuth2Credential caches tokens."""
|
|
with patch("httpx.post") as mock_post:
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {
|
|
"access_token": "oauth2-token",
|
|
"expires_in": 3600,
|
|
}
|
|
mock_response.raise_for_status = Mock()
|
|
mock_post.return_value = mock_response
|
|
|
|
cred = GenericOAuth2Credential(
|
|
client_id="test-client",
|
|
client_secret="test-secret",
|
|
token_url="https://example.com/oauth2/token",
|
|
)
|
|
|
|
# Multiple calls should only make one HTTP request
|
|
cred.get_token("test-scope")
|
|
cred.get_token("test-scope")
|
|
cred.get_token("test-scope")
|
|
|
|
assert mock_post.call_count == 1
|
|
|
|
|
|
class TestLiteLLMIntegration:
|
|
"""Tests for integration with litellm module."""
|
|
|
|
def test_proxy_auth_variable_exists(self):
|
|
"""Test that litellm.proxy_auth variable exists."""
|
|
import litellm
|
|
|
|
# Should be None by default
|
|
assert hasattr(litellm, "proxy_auth")
|
|
|
|
def test_proxy_auth_can_be_set(self):
|
|
"""Test that litellm.proxy_auth can be set to a ProxyAuthHandler."""
|
|
import litellm
|
|
|
|
original_value = litellm.proxy_auth
|
|
try:
|
|
cred = MockCredential()
|
|
handler = ProxyAuthHandler(credential=cred, scope="test")
|
|
litellm.proxy_auth = handler
|
|
|
|
assert litellm.proxy_auth is handler
|
|
finally:
|
|
litellm.proxy_auth = original_value
|