Files
litellm/tests/test_litellm/containers/test_container_integration.py
T
Alexsander Hamir f2c3a01a57 fix: make HTTPHandler mockable in OIDC secret manager tests (#19803)
* 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
2026-01-26 11:00:42 -08:00

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()