Skip to content

Commit 4cf5b1a

Browse files
authored
Merge pull request #1157 from tisnik/lcore-1238-integration-tests-for-models-endpoint
LCORE-1239: Integration tests for models REST API endpoint
2 parents 2cc0a39 + 3b4546f commit 4cf5b1a

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
"""Integration tests for the /models endpoint (using Responses API)."""
2+
3+
from typing import Any, Generator
4+
5+
import pytest
6+
from fastapi import Request
7+
from fastapi.exceptions import HTTPException
8+
from pytest_mock import AsyncMockType, MockerFixture
9+
from llama_stack_client import APIConnectionError
10+
11+
from models.requests import ModelFilter
12+
from app.endpoints.models import models_endpoint_handler
13+
from authentication.interface import AuthTuple
14+
from configuration import AppConfig
15+
16+
17+
@pytest.fixture(name="mock_llama_stack_client")
18+
def mock_llama_stack_client_fixture(
19+
mocker: MockerFixture,
20+
) -> Generator[Any, None, None]:
21+
"""Mock only the external Llama Stack client.
22+
23+
This is the only external dependency we mock for integration tests,
24+
as it represents an external service call.
25+
26+
Parameters:
27+
mocker (MockerFixture): pytest-mock fixture used to create and patch mocks.
28+
29+
Returns:
30+
mock_client: The mocked Llama Stack client instance configured as described above.
31+
"""
32+
# Patch in app.endpoints.models where it's actually used by models_endpoint_handler_base
33+
mock_holder_class = mocker.patch("app.endpoints.models.AsyncLlamaStackClientHolder")
34+
35+
mock_client = mocker.AsyncMock()
36+
37+
# Mock models list (required for model selection)
38+
mock_model1 = mocker.MagicMock()
39+
mock_model1.id = "test-provider/test-model-1"
40+
mock_model1.custom_metadata = {
41+
"provider_id": "test-provider",
42+
"model_type": "llm",
43+
}
44+
mock_model2 = mocker.MagicMock()
45+
mock_model2.id = "test-provider/test-model-2"
46+
mock_model2.custom_metadata = {
47+
"provider_id": "test-provider",
48+
"model_type": "embedding",
49+
}
50+
mock_client.models.list.return_value = [mock_model1, mock_model2]
51+
52+
# Create a mock holder instance
53+
mock_holder_instance = mock_holder_class.return_value
54+
mock_holder_instance.get_client.return_value = mock_client
55+
56+
yield mock_client
57+
58+
59+
@pytest.fixture(name="mock_llama_stack_client_failing")
60+
def mock_llama_stack_client_failing_fixture(
61+
mocker: MockerFixture,
62+
) -> Generator[Any, None, None]:
63+
"""Mock only the external Llama Stack client.
64+
65+
This is the only external dependency we mock for integration tests,
66+
as it represents an external service call.
67+
68+
Parameters:
69+
mocker (MockerFixture): pytest-mock fixture used to create and patch mocks.
70+
71+
Returns:
72+
mock_client: The mocked Llama Stack client instance configured as described above.
73+
"""
74+
# Patch in app.endpoints.models where it's actually used by models_endpoint_handler_base
75+
mock_holder_class = mocker.patch("app.endpoints.models.AsyncLlamaStackClientHolder")
76+
77+
mock_client = mocker.AsyncMock()
78+
79+
mock_client.models.list.side_effect = APIConnectionError(request=mocker.Mock())
80+
81+
# Create a mock holder instance
82+
mock_holder_instance = mock_holder_class.return_value
83+
mock_holder_instance.get_client.return_value = mock_client
84+
85+
yield mock_client
86+
87+
88+
@pytest.mark.asyncio
89+
async def test_models_list(
90+
test_config: AppConfig,
91+
mock_llama_stack_client: AsyncMockType,
92+
test_request: Request,
93+
test_auth: AuthTuple,
94+
) -> None:
95+
"""Test that models endpoint returns successful response.
96+
97+
This integration test verifies:
98+
- Model list handler
99+
100+
Parameters:
101+
test_config: Test configuration
102+
mock_llama_stack_client: Mocked Llama Stack client
103+
test_request: FastAPI request
104+
test_auth: noop authentication tuple
105+
"""
106+
_ = test_config
107+
_ = mock_llama_stack_client
108+
109+
response = await models_endpoint_handler(
110+
request=test_request,
111+
auth=test_auth,
112+
model_type=ModelFilter(model_type=None),
113+
)
114+
115+
# Verify response structure
116+
assert response is not None
117+
assert len(response.models) == 2
118+
assert response.models[0]["identifier"] == "test-provider/test-model-1"
119+
assert response.models[0]["api_model_type"] == "llm"
120+
assert response.models[1]["identifier"] == "test-provider/test-model-2"
121+
assert response.models[1]["api_model_type"] == "embedding"
122+
123+
124+
@pytest.mark.asyncio
125+
async def test_models_list_filter_model_type_llm(
126+
test_config: AppConfig,
127+
mock_llama_stack_client: AsyncMockType,
128+
test_request: Request,
129+
test_auth: AuthTuple,
130+
) -> None:
131+
"""Test that models endpoint returns successful response.
132+
133+
This integration test verifies:
134+
- Model list handler
135+
136+
Parameters:
137+
test_config: Test configuration
138+
mock_llama_stack_client: Mocked Llama Stack client
139+
test_request: FastAPI request
140+
test_auth: noop authentication tuple
141+
"""
142+
_ = test_config
143+
_ = mock_llama_stack_client
144+
145+
response = await models_endpoint_handler(
146+
request=test_request, auth=test_auth, model_type=ModelFilter(model_type="llm")
147+
)
148+
149+
# Verify response structure
150+
assert response is not None
151+
assert len(response.models) == 1
152+
assert response.models[0]["identifier"] == "test-provider/test-model-1"
153+
assert response.models[0]["api_model_type"] == "llm"
154+
155+
156+
@pytest.mark.asyncio
157+
async def test_models_list_filter_model_type_embedding(
158+
test_config: AppConfig,
159+
mock_llama_stack_client: AsyncMockType,
160+
test_request: Request,
161+
test_auth: AuthTuple,
162+
) -> None:
163+
"""Test that models endpoint returns successful response.
164+
165+
This integration test verifies:
166+
- Model list handler
167+
168+
Parameters:
169+
test_config: Test configuration
170+
mock_llama_stack_client: Mocked Llama Stack client
171+
test_request: FastAPI request
172+
test_auth: noop authentication tuple
173+
"""
174+
_ = test_config
175+
_ = mock_llama_stack_client
176+
177+
response = await models_endpoint_handler(
178+
request=test_request,
179+
auth=test_auth,
180+
model_type=ModelFilter(model_type="embedding"),
181+
)
182+
183+
# Verify response structure
184+
assert response is not None
185+
assert len(response.models) == 1
186+
assert response.models[0]["identifier"] == "test-provider/test-model-2"
187+
assert response.models[0]["api_model_type"] == "embedding"
188+
189+
190+
@pytest.mark.asyncio
191+
async def test_models_list_filter_model_type_unknown(
192+
test_config: AppConfig,
193+
mock_llama_stack_client: AsyncMockType,
194+
test_request: Request,
195+
test_auth: AuthTuple,
196+
) -> None:
197+
"""Test that models endpoint returns successful response.
198+
199+
This integration test verifies:
200+
- Model list handler
201+
202+
Parameters:
203+
test_config: Test configuration
204+
mock_llama_stack_client: Mocked Llama Stack client
205+
test_request: FastAPI request
206+
test_auth: noop authentication tuple
207+
"""
208+
_ = test_config
209+
_ = mock_llama_stack_client
210+
211+
response = await models_endpoint_handler(
212+
request=test_request,
213+
auth=test_auth,
214+
model_type=ModelFilter(model_type="foobar"),
215+
)
216+
217+
# Verify response structure
218+
assert response is not None
219+
assert len(response.models) == 0
220+
221+
222+
@pytest.mark.asyncio
223+
async def test_models_list_on_api_connection_error(
224+
test_config: AppConfig,
225+
mock_llama_stack_client_failing: AsyncMockType,
226+
test_request: Request,
227+
test_auth: AuthTuple,
228+
) -> None:
229+
"""Test that models endpoint raises HTTPException on API connection error.
230+
231+
This integration test verifies:
232+
- Model list handler
233+
- Error handling when Llama Stack is unreachable
234+
235+
Parameters:
236+
test_config: Test configuration
237+
mock_llama_stack_client_failing: Mocked Llama Stack client that raises APIConnectionError
238+
test_request: FastAPI request
239+
test_auth: noop authentication tuple
240+
"""
241+
_ = test_config
242+
_ = mock_llama_stack_client_failing
243+
244+
# we should catch HTTPException, not APIConnectionError!
245+
expected = "503: {'response': 'Unable to connect to Llama Stack', 'cause': 'Connection error.'}"
246+
with pytest.raises(HTTPException, match=expected):
247+
await models_endpoint_handler(
248+
request=test_request,
249+
auth=test_auth,
250+
model_type=ModelFilter(model_type=None),
251+
)

0 commit comments

Comments
 (0)