mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-11 07:05:00 +00:00
f2c3a01a57
* fix: make HTTPHandler mockable in OIDC secret manager tests - Add _get_oidc_http_handler() factory function to make HTTPHandler easily mockable in tests - Update test_oidc_github_success to patch factory function instead of HTTPHandler directly - Update Google OIDC tests for consistency - Fixes test_oidc_github_success failure where mock was bypassed This change allows tests to properly mock HTTPHandler instances used for OIDC token requests, fixing the test failure where the mock was not being used. * fix: patch base_llm_http_handler method directly in container tests - Use patch.object to patch container_create_handler method directly on the base_llm_http_handler instance instead of patching the module - Fixes test_provider_support[openai] failure where mock wasn't applied - Also fixes test_error_handling_integration with same approach The issue was that patching 'litellm.containers.main.base_llm_http_handler' didn't work because the module imports it with 'from litellm.main import', creating a local reference. Using patch.object patches the method on the actual object instance, which works regardless of import style. * fix: resolve flaky test_openai_env_base by clearing cache - Add cache clearing at start of test_openai_env_base to prevent cache pollution - Ensures no cached clients from previous tests interfere with respx mocks - Fixes intermittent failures where aiohttp transport was used instead of httpx - Test-only change with low risk, no production code modifications Resolves flaky test marked with @pytest.mark.flaky(retries=3, delay=1) Both parametrized versions (OPENAI_API_BASE and OPENAI_BASE_URL) now pass consistently * test: add explicit mock verification in test_provider_support - Capture mock handler with 'as mock_handler' for explicit validation - Add assert_called_once() to verify mock was actually used - Ensures test verifies no real API calls are made - Follows same pattern as test_openai_env_base validation
397 lines
15 KiB
Python
397 lines
15 KiB
Python
import json
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import httpx
|
|
|
|
sys.path.insert(
|
|
0, os.path.abspath("../../..")
|
|
) # Adds the parent directory to the system path
|
|
|
|
import litellm
|
|
from litellm.types.containers.main import ContainerObject, ContainerListResponse, DeleteContainerResult
|
|
from litellm.containers.main import (
|
|
create_container, acreate_container,
|
|
list_containers, alist_containers,
|
|
retrieve_container, aretrieve_container,
|
|
delete_container, adelete_container
|
|
)
|
|
|
|
|
|
class TestContainerIntegration:
|
|
"""Integration tests for the complete container API flow."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
# Mock environment variable for API key
|
|
os.environ["OPENAI_API_KEY"] = "sk-test123"
|
|
|
|
def teardown_method(self):
|
|
"""Clean up after tests."""
|
|
if "OPENAI_API_KEY" in os.environ:
|
|
del os.environ["OPENAI_API_KEY"]
|
|
|
|
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
|
|
def test_container_create_full_flow(self, mock_http_handler):
|
|
"""Test the complete container creation flow with mocked HTTP."""
|
|
# Setup mock HTTP response
|
|
mock_response = MagicMock(spec=httpx.Response)
|
|
mock_response.json.return_value = {
|
|
"id": "cntr_integration_test",
|
|
"object": "container",
|
|
"created_at": 1747857508,
|
|
"status": "running",
|
|
"expires_after": {"anchor": "last_active_at", "minutes": 20},
|
|
"last_active_at": 1747857508,
|
|
"name": "Integration Test Container"
|
|
}
|
|
mock_response.status_code = 200
|
|
|
|
# Mock the HTTP handler
|
|
mock_client = MagicMock()
|
|
mock_client.post.return_value = mock_response
|
|
mock_http_handler.return_value = mock_client
|
|
|
|
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
|
|
mock_get_client.return_value = mock_client
|
|
|
|
# Execute
|
|
response = create_container(
|
|
name="Integration Test Container",
|
|
expires_after={"anchor": "last_active_at", "minutes": 20},
|
|
custom_llm_provider="openai"
|
|
)
|
|
|
|
# Verify
|
|
assert isinstance(response, ContainerObject)
|
|
assert response.id == "cntr_integration_test"
|
|
assert response.name == "Integration Test Container"
|
|
assert response.status == "running"
|
|
|
|
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
|
|
def test_container_list_full_flow(self, mock_http_handler):
|
|
"""Test the complete container listing flow with mocked HTTP."""
|
|
# Setup mock HTTP response
|
|
mock_response = MagicMock(spec=httpx.Response)
|
|
mock_response.json.return_value = {
|
|
"object": "list",
|
|
"data": [
|
|
{
|
|
"id": "cntr_list_1",
|
|
"object": "container",
|
|
"created_at": 1747857508,
|
|
"status": "running",
|
|
"expires_after": {"anchor": "last_active_at", "minutes": 20},
|
|
"last_active_at": 1747857508,
|
|
"name": "List Container 1"
|
|
},
|
|
{
|
|
"id": "cntr_list_2",
|
|
"object": "container",
|
|
"created_at": 1747857600,
|
|
"status": "running",
|
|
"expires_after": {"anchor": "last_active_at", "minutes": 15},
|
|
"last_active_at": 1747857600,
|
|
"name": "List Container 2"
|
|
}
|
|
],
|
|
"first_id": "cntr_list_1",
|
|
"last_id": "cntr_list_2",
|
|
"has_more": False
|
|
}
|
|
mock_response.status_code = 200
|
|
|
|
# Mock the HTTP handler
|
|
mock_client = MagicMock()
|
|
mock_client.get.return_value = mock_response
|
|
mock_http_handler.return_value = mock_client
|
|
|
|
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
|
|
mock_get_client.return_value = mock_client
|
|
|
|
# Execute
|
|
response = list_containers(
|
|
limit=10,
|
|
order="desc",
|
|
custom_llm_provider="openai"
|
|
)
|
|
|
|
# Verify
|
|
assert isinstance(response, ContainerListResponse)
|
|
assert len(response.data) == 2
|
|
assert response.data[0].id == "cntr_list_1"
|
|
assert response.data[1].id == "cntr_list_2"
|
|
assert response.has_more == False
|
|
|
|
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
|
|
def test_container_retrieve_full_flow(self, mock_http_handler):
|
|
"""Test the complete container retrieval flow with mocked HTTP."""
|
|
container_id = "cntr_retrieve_integration"
|
|
|
|
# Setup mock HTTP response
|
|
mock_response = MagicMock(spec=httpx.Response)
|
|
mock_response.json.return_value = {
|
|
"id": container_id,
|
|
"object": "container",
|
|
"created_at": 1747857508,
|
|
"status": "running",
|
|
"expires_after": {"anchor": "last_active_at", "minutes": 20},
|
|
"last_active_at": 1747857508,
|
|
"name": "Retrieved Integration Container"
|
|
}
|
|
mock_response.status_code = 200
|
|
|
|
# Mock the HTTP handler
|
|
mock_client = MagicMock()
|
|
mock_client.get.return_value = mock_response
|
|
mock_http_handler.return_value = mock_client
|
|
|
|
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
|
|
mock_get_client.return_value = mock_client
|
|
|
|
# Execute
|
|
response = retrieve_container(
|
|
container_id=container_id,
|
|
custom_llm_provider="openai"
|
|
)
|
|
|
|
# Verify
|
|
assert isinstance(response, ContainerObject)
|
|
assert response.id == container_id
|
|
assert response.name == "Retrieved Integration Container"
|
|
|
|
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
|
|
def test_container_delete_full_flow(self, mock_http_handler):
|
|
"""Test the complete container deletion flow with mocked HTTP."""
|
|
container_id = "cntr_delete_integration"
|
|
|
|
# Setup mock HTTP response
|
|
mock_response = MagicMock(spec=httpx.Response)
|
|
mock_response.json.return_value = {
|
|
"id": container_id,
|
|
"object": "container.deleted",
|
|
"deleted": True
|
|
}
|
|
mock_response.status_code = 200
|
|
|
|
# Mock the HTTP handler
|
|
mock_client = MagicMock()
|
|
mock_client.delete.return_value = mock_response
|
|
mock_http_handler.return_value = mock_client
|
|
|
|
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
|
|
mock_get_client.return_value = mock_client
|
|
|
|
# Execute
|
|
response = delete_container(
|
|
container_id=container_id,
|
|
custom_llm_provider="openai"
|
|
)
|
|
|
|
# Verify
|
|
assert isinstance(response, DeleteContainerResult)
|
|
assert response.id == container_id
|
|
assert response.deleted == True
|
|
assert response.object == "container.deleted"
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('litellm.llms.custom_httpx.llm_http_handler.AsyncHTTPHandler')
|
|
async def test_async_container_create_full_flow(self, mock_async_http_handler):
|
|
"""Test the complete async container creation flow with mocked HTTP."""
|
|
# Setup mock HTTP response
|
|
mock_response = MagicMock(spec=httpx.Response)
|
|
mock_response.json.return_value = {
|
|
"id": "cntr_async_integration",
|
|
"object": "container",
|
|
"created_at": 1747857508,
|
|
"status": "running",
|
|
"expires_after": {"anchor": "last_active_at", "minutes": 30},
|
|
"last_active_at": 1747857508,
|
|
"name": "Async Integration Container"
|
|
}
|
|
mock_response.status_code = 200
|
|
|
|
# Mock the async HTTP handler
|
|
mock_client = MagicMock()
|
|
|
|
async def mock_post(*args, **kwargs):
|
|
return mock_response
|
|
|
|
mock_client.post = mock_post
|
|
mock_async_http_handler.return_value = mock_client
|
|
|
|
with patch('litellm.llms.custom_httpx.llm_http_handler.get_async_httpx_client') as mock_get_async_client:
|
|
mock_get_async_client.return_value = mock_client
|
|
|
|
# Execute
|
|
response = await acreate_container(
|
|
name="Async Integration Container",
|
|
expires_after={"anchor": "last_active_at", "minutes": 30},
|
|
custom_llm_provider="openai"
|
|
)
|
|
|
|
# Verify
|
|
assert isinstance(response, ContainerObject)
|
|
assert response.id == "cntr_async_integration"
|
|
assert response.name == "Async Integration Container"
|
|
|
|
@pytest.mark.asyncio
|
|
@patch('litellm.llms.custom_httpx.llm_http_handler.AsyncHTTPHandler')
|
|
async def test_async_container_list_full_flow(self, mock_async_http_handler):
|
|
"""Test the complete async container listing flow with mocked HTTP."""
|
|
# Setup mock HTTP response
|
|
mock_response = MagicMock(spec=httpx.Response)
|
|
mock_response.json.return_value = {
|
|
"object": "list",
|
|
"data": [
|
|
{
|
|
"id": "cntr_async_list",
|
|
"object": "container",
|
|
"created_at": 1747857508,
|
|
"status": "running",
|
|
"expires_after": {"anchor": "last_active_at", "minutes": 25},
|
|
"last_active_at": 1747857508,
|
|
"name": "Async List Container"
|
|
}
|
|
],
|
|
"first_id": "cntr_async_list",
|
|
"last_id": "cntr_async_list",
|
|
"has_more": False
|
|
}
|
|
mock_response.status_code = 200
|
|
|
|
# Mock the async HTTP handler
|
|
mock_client = MagicMock()
|
|
|
|
async def mock_get(*args, **kwargs):
|
|
return mock_response
|
|
|
|
mock_client.get = mock_get
|
|
mock_async_http_handler.return_value = mock_client
|
|
|
|
with patch('litellm.llms.custom_httpx.llm_http_handler.get_async_httpx_client') as mock_get_async_client:
|
|
mock_get_async_client.return_value = mock_client
|
|
|
|
# Execute
|
|
response = await alist_containers(
|
|
limit=5,
|
|
custom_llm_provider="openai"
|
|
)
|
|
|
|
# Verify
|
|
assert isinstance(response, ContainerListResponse)
|
|
assert len(response.data) == 1
|
|
assert response.data[0].id == "cntr_async_list"
|
|
|
|
def test_container_workflow_simulation(self):
|
|
"""Test a complete workflow: create -> list -> retrieve -> delete."""
|
|
container_id = "cntr_workflow_test"
|
|
|
|
# Mock all HTTP responses
|
|
create_response = MagicMock(spec=httpx.Response)
|
|
create_response.json.return_value = {
|
|
"id": container_id,
|
|
"object": "container",
|
|
"created_at": 1747857508,
|
|
"status": "running",
|
|
"expires_after": {"anchor": "last_active_at", "minutes": 20},
|
|
"last_active_at": 1747857508,
|
|
"name": "Workflow Test Container"
|
|
}
|
|
|
|
list_response = MagicMock(spec=httpx.Response)
|
|
list_response.json.return_value = {
|
|
"object": "list",
|
|
"data": [create_response.json.return_value],
|
|
"first_id": container_id,
|
|
"last_id": container_id,
|
|
"has_more": False
|
|
}
|
|
|
|
retrieve_response = create_response # Same as create
|
|
|
|
delete_response = MagicMock(spec=httpx.Response)
|
|
delete_response.json.return_value = {
|
|
"id": container_id,
|
|
"object": "container.deleted",
|
|
"deleted": True
|
|
}
|
|
|
|
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
|
|
# Setup different responses for different operations
|
|
mock_handler.container_create_handler.return_value = ContainerObject(**create_response.json.return_value)
|
|
mock_handler.container_list_handler.return_value = ContainerListResponse(**list_response.json.return_value)
|
|
mock_handler.container_retrieve_handler.return_value = ContainerObject(**retrieve_response.json.return_value)
|
|
mock_handler.container_delete_handler.return_value = DeleteContainerResult(**delete_response.json.return_value)
|
|
|
|
# Execute workflow
|
|
# 1. Create container
|
|
created = create_container(
|
|
name="Workflow Test Container",
|
|
custom_llm_provider="openai"
|
|
)
|
|
assert created.id == container_id
|
|
|
|
# 2. List containers (should include our created one)
|
|
containers = list_containers(custom_llm_provider="openai")
|
|
assert len(containers.data) == 1
|
|
assert containers.data[0].id == container_id
|
|
|
|
# 3. Retrieve specific container
|
|
retrieved = retrieve_container(
|
|
container_id=container_id,
|
|
custom_llm_provider="openai"
|
|
)
|
|
assert retrieved.id == container_id
|
|
assert retrieved.name == "Workflow Test Container"
|
|
|
|
# 4. Delete container
|
|
deleted = delete_container(
|
|
container_id=container_id,
|
|
custom_llm_provider="openai"
|
|
)
|
|
assert deleted.id == container_id
|
|
assert deleted.deleted == True
|
|
|
|
def test_error_handling_integration(self):
|
|
"""Test error handling in the integration flow."""
|
|
# Simulate an API error
|
|
api_error = litellm.APIError(
|
|
status_code=400,
|
|
message="API Error occurred",
|
|
llm_provider="openai",
|
|
model=""
|
|
)
|
|
|
|
with patch.object(litellm.main.base_llm_http_handler, 'container_create_handler', side_effect=api_error):
|
|
with pytest.raises(litellm.APIError):
|
|
create_container(
|
|
name="Error Test Container",
|
|
custom_llm_provider="openai"
|
|
)
|
|
|
|
@pytest.mark.parametrize("provider", ["openai"])
|
|
def test_provider_support(self, provider):
|
|
"""Test that the container API works with supported providers."""
|
|
mock_response = ContainerObject(
|
|
id="cntr_provider_test",
|
|
object="container",
|
|
created_at=1747857508,
|
|
status="running",
|
|
expires_after={"anchor": "last_active_at", "minutes": 20},
|
|
last_active_at=1747857508,
|
|
name="Provider Test Container"
|
|
)
|
|
|
|
with patch.object(litellm.main.base_llm_http_handler, 'container_create_handler', return_value=mock_response) as mock_handler:
|
|
response = create_container(
|
|
name="Provider Test Container",
|
|
custom_llm_provider=provider
|
|
)
|
|
|
|
assert response.name == "Provider Test Container"
|
|
# Verify the mock was actually called (not making real API calls)
|
|
mock_handler.assert_called_once()
|