From 899594d135ee97fcc4f6c447e516b3bb7f22efab Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 17 Dec 2025 09:19:39 +0100 Subject: [PATCH 1/2] Docstrings in app endpoint handlers unit tests --- tests/unit/app/endpoints/README.md | 3 + tests/unit/app/endpoints/test_authorized.py | 14 +- .../unit/app/endpoints/test_conversations.py | 119 ++++++++- .../app/endpoints/test_conversations_v2.py | 46 +++- tests/unit/app/endpoints/test_health.py | 13 +- tests/unit/app/endpoints/test_info.py | 13 +- tests/unit/app/endpoints/test_models.py | 12 +- tests/unit/app/endpoints/test_query.py | 149 ++++++++++- tests/unit/app/endpoints/test_query_v2.py | 9 +- tests/unit/app/endpoints/test_rags.py | 12 + tests/unit/app/endpoints/test_shields.py | 37 ++- .../app/endpoints/test_streaming_query.py | 237 +++++++++++++++++- .../app/endpoints/test_streaming_query_v2.py | 62 ++++- tests/unit/app/endpoints/test_tools.py | 10 +- 14 files changed, 701 insertions(+), 35 deletions(-) diff --git a/tests/unit/app/endpoints/README.md b/tests/unit/app/endpoints/README.md index 89d6a35b3..691951f17 100644 --- a/tests/unit/app/endpoints/README.md +++ b/tests/unit/app/endpoints/README.md @@ -42,6 +42,9 @@ Unit tests for the /query (v2) REST API endpoint using Responses API. ## [test_rags.py](test_rags.py) Unit tests for the /rags REST API endpoints. +## [test_rlsapi_v1.py](test_rlsapi_v1.py) +Unit tests for the rlsapi v1 /infer REST API endpoint. + ## [test_root.py](test_root.py) Unit tests for the / endpoint handler. diff --git a/tests/unit/app/endpoints/test_authorized.py b/tests/unit/app/endpoints/test_authorized.py index fa140a3f4..496154a69 100644 --- a/tests/unit/app/endpoints/test_authorized.py +++ b/tests/unit/app/endpoints/test_authorized.py @@ -47,7 +47,19 @@ async def test_authorized_unauthorized() -> None: @pytest.mark.asyncio async def test_authorized_dependency_unauthorized() -> None: - """Test that auth dependency raises HTTPException with 403 for unauthorized access.""" + """Test that auth dependency raises HTTPException with 403 for unauthorized access. + + Verify extract_user_token raises HTTPException with status code 401 and the + expected detail for missing or malformed Authorization headers. + + Checks two scenarios: + - Missing Authorization header: HTTPException.status_code == 401, + detail["response"] == "Missing or invalid credentials provided by + client", detail["cause"] == "No Authorization header found". + - Invalid Authorization format: HTTPException.status_code == 401, + detail["response"] == "Missing or invalid credentials provided by + client", detail["cause"] == "No token found in Authorization header". + """ # Test the auth utility function that would be called by auth dependencies # This simulates the unauthorized scenario that would prevent the handler from being called diff --git a/tests/unit/app/endpoints/test_conversations.py b/tests/unit/app/endpoints/test_conversations.py index fcb0cb545..7e12da21a 100644 --- a/tests/unit/app/endpoints/test_conversations.py +++ b/tests/unit/app/endpoints/test_conversations.py @@ -34,7 +34,13 @@ @pytest.fixture def dummy_request() -> Request: - """Mock request object for testing.""" + """Mock request object for testing. + + Create a mock FastAPI Request configured for tests with full authorization. + + The returned Request has state.authorized_actions set to a set containing + every member of Action. + """ request = Request( scope={ "type": "http", @@ -56,7 +62,35 @@ def create_mock_conversation( last_used_provider: str, topic_summary: Optional[str] = None, ) -> MockType: - """Helper function to create a mock conversation object with all required attributes.""" + """Helper function to create a mock conversation object with all required attributes. + + Create a mock conversation object with the attributes used by the + conversations list and detail tests. + + The returned mock has the following attributes: + - id: the conversation identifier (string) + - created_at.isoformat(): returns the provided created_at string + - last_message_at.isoformat(): returns the provided last_message_at string + - message_count: number of messages in the conversation + - last_used_model: model identifier last used in the conversation + - last_used_provider: provider identifier last used in the conversation + - topic_summary: optional topic summary (may be None or empty string) + + Parameters: + mocker (MockerFixture): pytest mocker fixture used to build the mock object. + conversation_id (str): Conversation identifier to assign to the mock. + created_at (str): ISO-formatted created-at timestamp to be returned by + created_at.isoformat(). + last_message_at (str): ISO-formatted last-message timestamp to be + returned by last_message_at.isoformat(). + message_count (int): Message count to assign to the mock. + last_used_model (str): Last used model string to assign to the mock. + last_used_provider (str): Last used provider string to assign to the mock. + topic_summary (Optional[str]): Optional topic summary to assign to the mock. + + Returns: + mock_conversation: A mock object configured with the above attributes. + """ mock_conversation = mocker.Mock() mock_conversation.id = conversation_id mock_conversation.created_at = mocker.Mock() @@ -73,7 +107,20 @@ def create_mock_conversation( def mock_database_session( mocker: MockerFixture, query_result: Optional[list[MockType]] = None ) -> MockType: - """Helper function to mock get_session with proper context manager support.""" + """Helper function to mock get_session with proper context manager support. + + Create and patch a mocked database session and a context-manager-compatible get_session. + + Parameters: + mocker (pytest.MockerFixture): Fixture used to create and patch mocks. + query_result (Optional[list]): If provided, configures the + session.query().all() and session.query().filter_by().all() to return + this list. + + Returns: + Mock: The mocked session object that will be yielded by the patched + get_session context manager. + """ mock_session = mocker.Mock() if query_result is not None: # Mock both the filtered and unfiltered query paths @@ -94,7 +141,16 @@ def mock_database_session( @pytest.fixture(name="setup_configuration") def setup_configuration_fixture() -> AppConfig: - """Set up configuration for tests.""" + """Set up configuration for tests. + + Create an AppConfig prepopulated with test-friendly default settings. + + Returns: + AppConfig: An AppConfig instance initialized from a dictionary + containing defaults suitable for tests (local service host/port, + disabled auth and user-data collection, test Llama Stack API key and + URL, and single worker). + """ config_dict: dict[str, Any] = { "name": "test", "service": { @@ -123,7 +179,29 @@ def setup_configuration_fixture() -> AppConfig: @pytest.fixture(name="mock_session_data") def mock_session_data_fixture() -> dict[str, Any]: - """Create mock session data for testing.""" + """Create mock session data for testing. + + Provide a representative mock session data payload used by tests to + simulate a conversation session. + + The returned dictionary contains: + - session_id: conversation identifier. + - session_name: human-readable session name. + - started_at: ISO 8601 timestamp when the session started. + - turns: list of turn objects; each turn includes: + - turn_id: identifier for the turn. + - input_messages: list of input message objects with `content`, `role`, + and optional `context`. + - output_message: assistant response object with `content`, `role`, and + auxiliary fields (e.g., `stop_reason`, `tool_calls`) that tests + expect to be filtered by simplification logic. + - started_at / completed_at: ISO 8601 timestamps for the turn. + - steps: detailed internal steps included to verify they are removed by simplification. + + Returns: + dict: A mock session data structure matching the shape produced by the + Llama Stack client for use in unit tests. + """ return { "session_id": VALID_CONVERSATION_ID, "session_name": "test-session", @@ -165,7 +243,17 @@ def mock_session_data_fixture() -> dict[str, Any]: @pytest.fixture(name="expected_chat_history") def expected_chat_history_fixture() -> list[dict[str, Any]]: - """Create expected simplified chat history for testing.""" + """Create expected simplified chat history for testing. + + Expected simplified chat history used by tests. + + Returns: + list[dict[str, Any]]: A list of conversation turns. Each turn contains: + - messages: list of message dicts with `content` (str) and `type` + (`"user"` or `"assistant"`) + - started_at: ISO 8601 UTC timestamp string for the turn start + - completed_at: ISO 8601 UTC timestamp string for the turn end + """ return [ { "messages": [ @@ -188,7 +276,14 @@ def expected_chat_history_fixture() -> list[dict[str, Any]]: @pytest.fixture(name="mock_conversation") def mock_conversation_fixture() -> UserConversation: - """Create a mock UserConversation object for testing.""" + """Create a mock UserConversation object for testing. + + Returns: + mock_conv (UserConversation): A UserConversation initialized with + VALID_CONVERSATION_ID, user_id set to "another_user", message_count 2, + last_used_model "mock-model", last_used_provider "mock-provider", and + topic_summary "Mock topic". + """ mock_conv = UserConversation() mock_conv.id = VALID_CONVERSATION_ID mock_conv.user_id = "another_user" # Different from test auth @@ -366,7 +461,15 @@ async def test_llama_stack_not_found_error( setup_configuration: AppConfig, dummy_request: Request, ) -> None: - """Test the endpoint when LlamaStack returns NotFoundError.""" + """Test the endpoint when LlamaStack returns NotFoundError. + + Verify the GET /conversations/{conversation_id} handler raises an HTTP + 404 when the Llama Stack client reports the session as not found. + + Asserts that the raised HTTPException contains a response message + indicating the conversation was not found and a cause that includes + "does not exist" and the conversation ID. + """ mock_authorization_resolvers(mocker) mocker.patch("app.endpoints.conversations.configuration", setup_configuration) mocker.patch("app.endpoints.conversations.check_suid", return_value=True) diff --git a/tests/unit/app/endpoints/test_conversations_v2.py b/tests/unit/app/endpoints/test_conversations_v2.py index d52db81c9..1ee7f8d86 100644 --- a/tests/unit/app/endpoints/test_conversations_v2.py +++ b/tests/unit/app/endpoints/test_conversations_v2.py @@ -135,7 +135,16 @@ def test_transform_message_with_empty_referenced_documents(self) -> None: @pytest.fixture def mock_configuration(mocker: MockerFixture) -> MockType: - """Mock configuration with conversation cache.""" + """Mock configuration with conversation cache. + + Create a mocked configuration object with a mocked `conversation_cache` attribute. + + Parameters: + mocker (pytest.MockFixture): The pytest-mock fixture used to create mocks. + + Returns: + Mock: A mock configuration object whose `conversation_cache` attribute is a mock. + """ mock_config = mocker.Mock() mock_cache = mocker.Mock() mock_config.conversation_cache = mock_cache @@ -303,7 +312,15 @@ async def test_successful_retrieval_empty_list( async def test_with_skip_userid_check( self, mocker: MockerFixture, mock_configuration: MockType ) -> None: - """Test the endpoint with skip_userid_check flag.""" + """Test the endpoint with skip_userid_check flag. + + Verify the conversations list handler forwards the skip_userid_check + flag from the auth tuple to the conversation cache. + + Sets up a mocked configuration and auth tuple with the skip flag set to + True, invokes the handler, and asserts that `conversation_cache.list` + is called with the user ID and `True`. + """ mock_authorization_resolvers(mocker) mocker.patch("app.endpoints.conversations_v2.configuration", mock_configuration) mock_configuration.conversation_cache.list.return_value = [] @@ -375,7 +392,18 @@ async def test_invalid_conversation_id_format( async def test_conversation_cache_not_configured( self, mocker: MockerFixture ) -> None: - """Test the endpoint when conversation cache is not configured.""" + """Test the endpoint when conversation cache is not configured. + + Verify the conversation GET endpoint raises an HTTP 500 error when the + conversation cache is not configured. + + Patches the application configuration so + `conversation_cache_configuration.type` is None and ensures + `check_suid` returns True, then calls + `get_conversation_endpoint_handler` and asserts that it raises an + `HTTPException` with status code 500 and a response detail containing + "Conversation cache not configured". + """ mock_authorization_resolvers(mocker) mock_config = mocker.Mock() mock_config.conversation_cache_configuration = mocker.Mock() @@ -632,7 +660,17 @@ async def test_unsuccessful_deletion( async def test_with_skip_userid_check( self, mocker: MockerFixture, mock_configuration: MockType ) -> None: - """Test the endpoint with skip_userid_check flag.""" + """Test the endpoint with skip_userid_check flag. + + Verifies that providing an auth tuple with the skip-userid flag set + causes the conversation delete handler to call the cache delete method + with the skip flag. + + This test patches configuration and SUID validation, supplies an auth + tuple where the third element is True, invokes + delete_conversation_endpoint_handler, and asserts the cache.delete was + called with (user_id, conversation_id, True). + """ mock_authorization_resolvers(mocker) mocker.patch("app.endpoints.conversations_v2.configuration", mock_configuration) mocker.patch("app.endpoints.conversations_v2.check_suid", return_value=True) diff --git a/tests/unit/app/endpoints/test_health.py b/tests/unit/app/endpoints/test_health.py index d9532d3c0..3c5b7dcc7 100644 --- a/tests/unit/app/endpoints/test_health.py +++ b/tests/unit/app/endpoints/test_health.py @@ -126,7 +126,18 @@ class TestGetProvidersHealthStatuses: """Test cases for the get_providers_health_statuses function.""" async def test_get_providers_health_statuses(self, mocker: MockerFixture) -> None: - """Test get_providers_health_statuses with healthy providers.""" + """Test get_providers_health_statuses with healthy providers. + + Verify get_providers_health_statuses returns a ProviderHealthStatus + entry for each provider reported by the client. + + Mocks an AsyncLlamaStack client whose providers.list() returns three + providers with distinct health dicts, then asserts the function + produces three results with: + - provider1: status OK, message "All good" + - provider2: status NOT_IMPLEMENTED, message "Provider does not implement health check" + - unhealthy_provider: status ERROR, message "Connection failed" + """ # Mock the imports mock_lsc = mocker.patch("client.AsyncLlamaStackClientHolder.get_client") diff --git a/tests/unit/app/endpoints/test_info.py b/tests/unit/app/endpoints/test_info.py index 9800be084..71ffafec4 100644 --- a/tests/unit/app/endpoints/test_info.py +++ b/tests/unit/app/endpoints/test_info.py @@ -77,7 +77,18 @@ async def test_info_endpoint(mocker: MockerFixture) -> None: @pytest.mark.asyncio async def test_info_endpoint_connection_error(mocker: MockerFixture) -> None: - """Test the info endpoint handler.""" + """Test the info endpoint handler. + + Verify that info_endpoint_handler raises an HTTPException with + status 503 when the LlamaStack client cannot connect. + + Sets up application configuration and patches the LlamaStack + client so that calling its version inspection raises an + APIConnectionError, then asserts the raised HTTPException has + status code 503 and a detail payload containing a "response" of + "Service unavailable" and a "cause" that includes "Unable to + connect to Llama Stack". + """ mock_authorization_resolvers(mocker) # configuration for tests diff --git a/tests/unit/app/endpoints/test_models.py b/tests/unit/app/endpoints/test_models.py index fb2b65f6a..a477d7b28 100644 --- a/tests/unit/app/endpoints/test_models.py +++ b/tests/unit/app/endpoints/test_models.py @@ -44,7 +44,17 @@ async def test_models_endpoint_handler_configuration_not_loaded( async def test_models_endpoint_handler_configuration_loaded( mocker: MockerFixture, ) -> None: - """Test the models endpoint handler if configuration is loaded.""" + """Test the models endpoint handler if configuration is loaded. + + Verify the models endpoint raises HTTP 503 when configuration is loaded but + the Llama Stack client cannot connect. + + Loads an AppConfig from a test dictionary, patches the endpoint's + configuration and AsyncLlamaStackClientHolder so that get_client raises + APIConnectionError, issues a request with an authorization header, and + asserts that calling the handler raises an HTTPException with status 503 + and a detail response of "Unable to connect to Llama Stack". + """ mock_authorization_resolvers(mocker) # configuration for tests diff --git a/tests/unit/app/endpoints/test_query.py b/tests/unit/app/endpoints/test_query.py index b12deea47..e8f2dd59d 100644 --- a/tests/unit/app/endpoints/test_query.py +++ b/tests/unit/app/endpoints/test_query.py @@ -57,7 +57,18 @@ @pytest.fixture def dummy_request() -> Request: - """Dummy request fixture for testing.""" + """Dummy request fixture for testing. + + Create a minimal FastAPI Request with test-ready authorization state. + + The returned Request has a minimal HTTP scope and a + `state.authorized_actions` attribute initialized to a set containing all + members of the `Action` enum, suitable for use in unit tests that require + an authenticated request context. + + Returns: + req (Request): FastAPI Request with `state.authorized_actions` set to `set(Action)`. + """ req = Request( scope={ "type": "http", @@ -69,7 +80,14 @@ def dummy_request() -> Request: def mock_metrics(mocker: MockerFixture) -> None: - """Helper function to mock metrics operations for query endpoints.""" + """Helper function to mock metrics operations for query endpoints. + + Configure the provided pytest-mock `mocker` to stub token metrics and + related metrics counters used by query endpoint tests. + + Patches the token metrics extraction helper and the LLM metrics counters so + tests can run without emitting real metrics. + """ mocker.patch( "app.endpoints.query.extract_and_update_token_metrics", return_value=TokenCounter(), @@ -81,7 +99,18 @@ def mock_metrics(mocker: MockerFixture) -> None: def mock_database_operations(mocker: MockerFixture) -> None: - """Helper function to mock database operations for query endpoints.""" + """Helper function to mock database operations for query endpoints. + + Patch common database operations used by query endpoint tests. + + This applies test-time patches so that conversation ownership checks + succeed, persistence of conversation details is stubbed out, and + `get_session` returns a context-manager mock whose + `query(...).filter_by(...).first()` returns `None`. + + Parameters: + mocker (MockerFixture): The pytest-mock fixture used to apply patches. + """ mocker.patch( "app.endpoints.query.validate_conversation_ownership", return_value=True ) @@ -97,7 +126,20 @@ def mock_database_operations(mocker: MockerFixture) -> None: @pytest.fixture(name="setup_configuration") def setup_configuration_fixture() -> AppConfig: - """Set up configuration for tests.""" + """Set up configuration for tests. + + Create a reusable application configuration tailored for unit tests. + + The returned AppConfig is initialized from a fixed dictionary that sets: + - a lightweight service configuration (localhost, port 8080, minimal workers, logging enabled), + - a test Llama Stack configuration (test API key and URL, not used as a library client), + - user data collection with transcripts disabled, + - an empty MCP servers list, + - a noop conversation cache. + + Returns: + AppConfig: an initialized configuration instance suitable for test fixtures. + """ config_dict: dict[Any, Any] = { "name": "test", "service": { @@ -185,7 +227,22 @@ async def _test_query_endpoint_handler( dummy_request: Request, store_transcript_to_file: bool = False, ) -> None: - """Test the query endpoint handler.""" + """Test the query endpoint handler. + + Exercise the query_endpoint_handler and assert observable outcomes for a + typical successful request. + + Calls query_endpoint_handler with mocked dependencies and verifies the + returned response and conversation_id match the agent summary, that a + CacheEntry with referenced documents is stored, and that transcript storage + is invoked only when transcripts are enabled in configuration. + + Parameters: + store_transcript_to_file (bool): When True, configuration reports + transcripts enabled and the test asserts store_transcript is called + with expected arguments; when False, asserts store_transcript is not + called. + """ mock_client = mocker.AsyncMock() mock_lsc = mocker.patch("client.AsyncLlamaStackClientHolder.get_client") mock_lsc.return_value = mock_client @@ -707,12 +764,31 @@ class MockShield: """Mock for Llama Stack shield to be used.""" def __init__(self, identifier: str) -> None: + """ + Initialize the instance with an identifying string. + + Parameters: + identifier (str): The identifier for this instance; saved to + the `identifier` attribute. + """ self.identifier = identifier def __str__(self) -> str: + """ + Return a human-readable name for this mock shield instance. + + Returns: + The string "MockShield". + """ return "MockShield" def __repr__(self) -> str: + """ + Return the developer-facing string representation for this MockShield. + + Returns: + str: The fixed representation "MockShield". + """ return "MockShield" mock_client, mock_agent = prepare_agent_mocks @@ -763,12 +839,30 @@ class MockShield: """Mock for Llama Stack shield to be used.""" def __init__(self, identifier: str): + """ + Initialize the instance with the provided identifier. + + Parameters: + identifier (str): Unique identifier for this object. + """ self.identifier = identifier def __str__(self) -> str: + """ + Return a human-readable name for this mock shield instance. + + Returns: + The string "MockShield". + """ return "MockShield" def __repr__(self) -> str: + """ + Return the developer-facing string representation for this MockShield. + + Returns: + str: The fixed representation "MockShield". + """ return "MockShield" mock_client, mock_agent = prepare_agent_mocks @@ -822,12 +916,31 @@ class MockShield: """Mock for Llama Stack shield to be used.""" def __init__(self, identifier: str) -> None: + """ + Initialize the instance with an identifying string. + + Parameters: + identifier (str): The identifier for this instance; saved to + the `identifier` attribute. + """ self.identifier = identifier def __str__(self) -> str: + """ + Return a human-readable name for this mock shield instance. + + Returns: + The string "MockShield". + """ return "MockShield" def __repr__(self) -> str: + """ + Return the developer-facing string representation for this MockShield. + + Returns: + str: The fixed representation "MockShield". + """ return "MockShield" mock_client, mock_agent = prepare_agent_mocks @@ -889,7 +1002,18 @@ def __repr__(self) -> str: async def test_retrieve_response_with_one_attachment( prepare_agent_mocks: AgentFixtures, mocker: MockerFixture ) -> None: - """Test the retrieve_response function.""" + """Test the retrieve_response function. + + Verifies that retrieve_response includes a single attachment as a document + when calling the agent and returns the LLM response and conversation id. + + Asserts that: + - The returned summary.llm_response matches the agent's output. + - The returned conversation_id matches the agent session's conversation id. + - The agent.create_turn is invoked once with the attachment converted to a + document dict containing `content` and `mime_type`, and with the expected + session_id, messages, stream, and toolgroups. + """ mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" mock_client.shields.list.return_value = [] @@ -1437,7 +1561,18 @@ def test_get_rag_toolgroups() -> None: async def test_query_endpoint_handler_on_connection_error( mocker: MockerFixture, dummy_request: Request ) -> None: - """Test the query endpoint handler.""" + """Test the query endpoint handler. + + Verifies that query_endpoint_handler raises an HTTPException with status + 503 when connecting to Llama Stack fails and that the failure metric is + incremented. + + The test simulates an APIConnectionError from the Llama Stack client, calls + query_endpoint_handler with a simple QueryRequest, and asserts that: + - an HTTPException is raised with status code 503 Service Unavailable, + - the exception detail is a dict containing response == "Unable to connect to Llama Stack", + - the llm failure metric counter's increment method was called once. + """ mock_metric = mocker.patch("metrics.llm_calls_failures_total") mocker.patch( diff --git a/tests/unit/app/endpoints/test_query_v2.py b/tests/unit/app/endpoints/test_query_v2.py index 38330eaaf..b9d3ec3f7 100644 --- a/tests/unit/app/endpoints/test_query_v2.py +++ b/tests/unit/app/endpoints/test_query_v2.py @@ -29,7 +29,14 @@ @pytest.fixture def dummy_request() -> Request: - """Create a dummy FastAPI Request object for testing.""" + """Create a dummy FastAPI Request object for testing. + + Create a minimal FastAPI Request object suitable for unit tests. + + Returns: + request (fastapi.Request): A Request constructed with a bare HTTP scope + (type "http") for use in tests. + """ req = Request(scope={"type": "http"}) return req diff --git a/tests/unit/app/endpoints/test_rags.py b/tests/unit/app/endpoints/test_rags.py index a3630dbed..c79365a28 100644 --- a/tests/unit/app/endpoints/test_rags.py +++ b/tests/unit/app/endpoints/test_rags.py @@ -71,12 +71,24 @@ class RagInfo: """RagInfo mock.""" def __init__(self, rag_id: str) -> None: + """ + Initialize a RagInfo instance with the given identifier. + + Parameters: + rag_id (str): The unique identifier for the RAG. + """ self.id = rag_id class RagList: """List of RAGs mock.""" def __init__(self) -> None: + """ + Initialize the object with a predefined list of RagInfo objects used as mock data. + + The instance attribute `data` contains three RagInfo instances with + fixed IDs simulating available RAG entries for tests. + """ self.data = [ RagInfo("vs_00000000-cafe-babe-0000-000000000000"), RagInfo("vs_7b52a8cf-0fa3-489c-beab-27e061d102f3"), diff --git a/tests/unit/app/endpoints/test_shields.py b/tests/unit/app/endpoints/test_shields.py index 4306aff40..d6fe7efb7 100644 --- a/tests/unit/app/endpoints/test_shields.py +++ b/tests/unit/app/endpoints/test_shields.py @@ -46,7 +46,16 @@ async def test_shields_endpoint_handler_configuration_not_loaded( async def test_shields_endpoint_handler_improper_llama_stack_configuration( mocker: MockerFixture, ) -> None: - """Test the shields endpoint handler if Llama Stack configuration is not proper.""" + """Test the shields endpoint handler if Llama Stack configuration is not proper. + + Verify shields_endpoint_handler returns an empty ShieldsResponse when Llama + Stack is configured minimally and the client provides no shields. + + Patches the endpoint configuration and client holder to supply a mocked + Llama Stack client whose `shields.list` returns an empty list, then calls + the handler with a test request and authorization tuple and asserts the + response is a ShieldsResponse with an empty `shields` list. + """ mock_authorization_resolvers(mocker) # configuration for tests @@ -106,7 +115,20 @@ async def test_shields_endpoint_handler_improper_llama_stack_configuration( async def test_shields_endpoint_handler_configuration_loaded( mocker: MockerFixture, ) -> None: - """Test the shields endpoint handler if configuration is loaded.""" + """Test the shields endpoint handler if configuration is loaded. + + Verify shields_endpoint_handler raises an HTTP 503 with detail "Unable to + connect to Llama Stack" when configuration is loaded but the Llama Stack + client is unreachable. + + Sets up an AppConfig from a valid configuration, patches the endpoint's + configuration and AsyncLlamaStackClientHolder to return a client whose + shields.list raises APIConnectionError, and asserts the handler raises an + HTTPException with status 503 and the expected detail. + + Parameters: + mocker (MockerFixture): pytest-mock fixture used to create patches and mocks. + """ mock_authorization_resolvers(mocker) # configuration for tests @@ -219,7 +241,16 @@ async def test_shields_endpoint_handler_unable_to_retrieve_shields_list( async def test_shields_endpoint_llama_stack_connection_error( mocker: MockerFixture, ) -> None: - """Test the shields endpoint when LlamaStack connection fails.""" + """Test the shields endpoint when LlamaStack connection fails. + + Verifies that the shields endpoint responds with HTTP 503 and an + appropriate cause when the Llama Stack client cannot be reached. + + Simulates the Llama Stack client raising an APIConnectionError and asserts + that calling the endpoint raises an HTTPException with status 503, a detail + response of "Service unavailable", and a detail cause that contains "Unable + to connect to Llama Stack". + """ mock_authorization_resolvers(mocker) # configuration for tests diff --git a/tests/unit/app/endpoints/test_streaming_query.py b/tests/unit/app/endpoints/test_streaming_query.py index 0280cb69b..a684dad95 100644 --- a/tests/unit/app/endpoints/test_streaming_query.py +++ b/tests/unit/app/endpoints/test_streaming_query.py @@ -53,6 +53,13 @@ class TextDelta: """Mock TextDelta for Agent API tests.""" def __init__(self, text: str, type: str = "text"): # noqa: A002 + """ + Initialize the object with textual content and a chunk type. + + Parameters: + text (str): The textual content for this instance. + type (str): The content type or category (for example, "text"). Defaults to "text". + """ self.text = text self.type = type @@ -61,6 +68,15 @@ class ToolCallDelta: """Mock ToolCallDelta for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -73,6 +89,15 @@ class TurnResponseEvent: """Mock TurnResponseEvent for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -81,6 +106,15 @@ class AgentTurnResponseStreamChunk: """Mock AgentTurnResponseStreamChunk for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -89,6 +123,15 @@ class AgentTurnResponseStepCompletePayload: """Mock AgentTurnResponseStepCompletePayload for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -97,6 +140,15 @@ class AgentTurnResponseStepProgressPayload: """Mock AgentTurnResponseStepProgressPayload for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -105,6 +157,15 @@ class AgentTurnResponseTurnAwaitingInputPayload: """Mock AgentTurnResponseTurnAwaitingInputPayload for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -113,6 +174,15 @@ class AgentTurnResponseTurnCompletePayload: """Mock AgentTurnResponseTurnCompletePayload for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -121,6 +191,15 @@ class AgentTurnResponseTurnStartPayload: """Mock AgentTurnResponseTurnStartPayload for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -129,6 +208,15 @@ class ToolExecutionStep: """Mock ToolExecutionStep for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -137,6 +225,15 @@ class ToolResponse: """Mock ToolResponse for Agent API tests.""" def __init__(self, **kwargs: Any): + """ + Initialize the instance by setting attributes from the provided keyword arguments. + + Parameters: + **kwargs: Any + Attribute names and values to assign to the instance. Each key in + `kwargs` becomes an attribute on the created object with the + corresponding value. + """ for key, value in kwargs.items(): setattr(self, key, value) @@ -152,7 +249,17 @@ def __init__(self, **kwargs: Any): def mock_database_operations(mocker: MockerFixture) -> None: - """Helper function to mock database operations for streaming query endpoints.""" + """Helper function to mock database operations for streaming query endpoints. + + Configure test mocks for conversation ownership validation and post-stream + cleanup used by streaming-query tests. + + Parameters: + mocker (MockerFixture): Pytest-mock fixture used to patch functions. + After calling this helper, `validate_conversation_ownership` is patched + to return `True` and `cleanup_after_streaming` is patched to an async + no-op. + """ mocker.patch( "app.endpoints.streaming_query.validate_conversation_ownership", return_value=True, @@ -203,7 +310,17 @@ def mock_metrics(mocker: MockerFixture) -> None: @pytest.fixture(autouse=True, name="setup_configuration") def setup_configuration_fixture() -> AppConfig: - """Set up configuration for tests.""" + """Set up configuration for tests. + + Create and initialize an AppConfig instance preconfigured for unit tests. + + The configuration uses a local service (localhost:8080), a test Llama Stack + API key and URL, disables user transcript collection, and sets a noop + conversation cache and empty MCP servers to avoid external dependencies. + + Returns: + AppConfig: An initialized AppConfig populated with the test settings. + """ config_dict = { "name": "test", "service": { @@ -275,6 +392,16 @@ async def test_streaming_query_endpoint_on_connection_error( # simulate situation when it is not possible to connect to Llama Stack def _raise_connection_error(*args: Any, **kwargs: Any) -> None: + """ + Raise an APIConnectionError unconditionally. + + Accepts any positional and keyword arguments and always raises an + APIConnectionError (with `request=None`), intended for use in tests to + simulate a connection failure. + + Raises: + APIConnectionError: Always raised to represent a client connection error. + """ raise APIConnectionError(request=None) # type: ignore[arg-type] mocker.patch( @@ -303,7 +430,16 @@ def _raise_connection_error(*args: Any, **kwargs: Any) -> None: # pylint: disable=too-many-locals async def _test_streaming_query_endpoint_handler(mocker: MockerFixture) -> None: - """Test the streaming query endpoint handler.""" + """ + Set up a simulated Llama Stack streaming response and verify the streaming-query endpoint. + + Mocks an AsyncLlamaStack client and retrieve_response to produce a sequence + of step_progress, step_complete, and turn_complete chunks, invokes + streaming_query_endpoint_handler, and asserts that the returned + StreamingResponse contains SSE start/token/end events, the final LLM + answer, seven streamed chunks, and two referenced documents with the second + titled "Doc2". + """ mock_client = mocker.AsyncMock() mock_async_lsc = mocker.patch("client.AsyncLlamaStackClientHolder.get_client") mock_async_lsc.return_value = mock_client @@ -495,7 +631,20 @@ async def test_streaming_query_endpoint_handler_store_transcript( async def test_retrieve_response_vector_db_available( prepare_agent_mocks: AgentFixtures, mocker: MockerFixture ) -> None: - """Test the retrieve_response function.""" + """Test the retrieve_response function. + + Verifies that retrieve_response detects available vector databases and + invokes the agent with appropriate toolgroups for a streaming query. + + Mocks an agent and client with one vector database present, patches + configuration and agent retrieval, then calls retrieve_response and + asserts: + - a streaming response object is returned (non-None), + - the conversation ID returned matches the agent's ID, + - the agent's create_turn is called once with the user message, streaming + enabled, no documents, and toolgroups derived from the detected vector + database. + """ mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" mock_client.shields.list.return_value = [] @@ -591,12 +740,30 @@ class MockShield: """Mock for Llama Stack shield to be used.""" def __init__(self, identifier: str) -> None: + """ + Initialize the instance with a unique identifier. + + Parameters: + identifier (str): A unique string used to identify this instance. + """ self.identifier = identifier def __str__(self) -> str: + """ + Provide a readable name for the mock shield. + + Returns: + str: The fixed string 'MockShield'. + """ return "MockShield" def __repr__(self) -> str: + """ + Provide a concise developer-facing representation for MockShield objects. + + Returns: + representation (str): The string "MockShield". + """ return "MockShield" mock_client, mock_agent = prepare_agent_mocks @@ -640,18 +807,48 @@ def __repr__(self) -> str: async def test_retrieve_response_two_available_shields( prepare_agent_mocks: AgentFixtures, mocker: MockerFixture ) -> None: - """Test the retrieve_response function.""" + """Test the retrieve_response function. + + Verifies retrieve_response uses available shields and starts a streaming + turn with expected arguments. + + Patches configuration and agent retrieval to provide a mocked client and + agent with two shields available, then calls retrieve_response and asserts: + - a non-None response is returned and the conversation ID matches the + mocked agent value, + - the agent's create_turn is invoked once with the user's message, the + mocked session_id, an empty documents list, stream=True, and + toolgroups=None. + """ class MockShield: """Mock for Llama Stack shield to be used.""" def __init__(self, identifier: str) -> None: + """ + Initialize the instance with a unique identifier. + + Parameters: + identifier (str): A unique string used to identify this instance. + """ self.identifier = identifier def __str__(self) -> str: + """ + Provide a readable name for the mock shield. + + Returns: + str: The fixed string 'MockShield'. + """ return "MockShield" def __repr__(self) -> str: + """ + Provide a concise developer-facing representation for MockShield objects. + + Returns: + representation (str): The string "MockShield". + """ return "MockShield" mock_client, mock_agent = prepare_agent_mocks @@ -704,12 +901,30 @@ class MockShield: """Mock for Llama Stack shield to be used.""" def __init__(self, identifier: str) -> None: + """ + Initialize the instance with a unique identifier. + + Parameters: + identifier (str): A unique string used to identify this instance. + """ self.identifier = identifier def __str__(self) -> str: + """ + Provide a readable name for the mock shield. + + Returns: + str: The fixed string 'MockShield'. + """ return "MockShield" def __repr__(self) -> str: + """ + Provide a concise developer-facing representation for MockShield objects. + + Returns: + representation (str): The string "MockShield". + """ return "MockShield" mock_client, mock_agent = prepare_agent_mocks @@ -825,7 +1040,17 @@ async def test_retrieve_response_with_one_attachment( async def test_retrieve_response_with_two_attachments( prepare_agent_mocks: AgentFixtures, mocker: MockerFixture ) -> None: - """Test the retrieve_response function.""" + """Test the retrieve_response function. + + Verifies that retrieve_response converts request attachments into document + inputs, calls the agent with streaming enabled, and returns the agent + response and conversation id. + + Asserts that: + - the returned conversation id matches the agent's id, + - the agent's create_turn is invoked once with stream=True, + - attachments are transformed into documents with the correct content and mime_type. + """ mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" mock_client.shields.list.return_value = [] diff --git a/tests/unit/app/endpoints/test_streaming_query_v2.py b/tests/unit/app/endpoints/test_streaming_query_v2.py index 9ba0900fc..92c1ecfe9 100644 --- a/tests/unit/app/endpoints/test_streaming_query_v2.py +++ b/tests/unit/app/endpoints/test_streaming_query_v2.py @@ -21,7 +21,14 @@ @pytest.fixture def dummy_request() -> Request: - """Create a dummy FastAPI Request for testing with authorized actions.""" + """Create a dummy FastAPI Request for testing with authorized actions. + + Create a FastAPI Request configured for tests with permissive RBAC. + + Returns: + Request: A FastAPI Request whose `state.authorized_actions` is set to a + set of all `Action` members. + """ req = Request(scope={"type": "http"}) # Provide a permissive authorized_actions set to satisfy RBAC check req.state.authorized_actions = set(Action) @@ -141,6 +148,26 @@ async def test_streaming_query_endpoint_handler_v2_success_yields_events( # Build a fake async stream of chunks async def fake_stream() -> AsyncIterator[SimpleNamespace]: + """ + Produce a fake asynchronous stream of response events used for testing streaming endpoints. + + Yields SimpleNamespace objects that emulate event frames from a + streaming responses API, including: + - a "response.created" event with a conversation id, + - content and text delta events ("response.content_part.added", + "response.output_text.delta"), + - function call events ("response.output_item.added", + "response.function_call_arguments.delta", + "response.function_call_arguments.done"), + - a final "response.output_text.done" event and a "response.completed" event. + + Returns: + AsyncIterator[SimpleNamespace]: An async iterator that yields + event-like SimpleNamespace objects representing the streamed + response frames; the final yielded response contains an `output` + attribute (an empty list) to allow shield violation detection in + tests. + """ yield SimpleNamespace( type="response.created", response=SimpleNamespace(id="conv-xyz") ) @@ -219,6 +246,13 @@ async def test_streaming_query_endpoint_handler_v2_api_connection_error( mocker.patch("app.endpoints.streaming_query.check_configuration_loaded") def _raise(*_a: Any, **_k: Any) -> None: + """ + Always raises an APIConnectionError with its `request` attribute set to None. + + Raises: + APIConnectionError: Raised every time the function is called; the + exception's `request` is None. + """ raise APIConnectionError(request=None) # type: ignore[arg-type] mocker.patch("client.AsyncLlamaStackClientHolder.get_client", side_effect=_raise) @@ -362,6 +396,18 @@ async def test_streaming_response_detects_shield_violation( # Build a fake async stream with shield violation async def fake_stream_with_violation() -> AsyncIterator[SimpleNamespace]: + """ + Produce an async iterator of SimpleNamespace events that simulates a streaming response. + + Yields: + AsyncIterator[SimpleNamespace]: Sequence of event objects in order: + - type="response.created" with a `response.id` + - type="response.output_text.delta" with a `delta` fragment + - type="response.output_text.done" with a `text` final chunk + - type="response.completed" whose `response.output` contains a + message object with a `refusal` field indicating a safety + policy violation + """ yield SimpleNamespace( type="response.created", response=SimpleNamespace(id="conv-violation") ) @@ -454,6 +500,20 @@ async def test_streaming_response_no_shield_violation( # Build a fake async stream without violation async def fake_stream_without_violation() -> AsyncIterator[SimpleNamespace]: + """ + Produce a deterministic sequence of streaming response events that end with a message. + + Yields four events in order: + - `response.created` with a response id, + - `response.output_text.delta` with a text fragment, + - `response.output_text.done` with the final text, + - `response.completed` whose `response.output` contains an assistant + message where `refusal` is `None`. + + Returns: + An iterator yielding SimpleNamespace objects representing the + streaming events of a successful response with no refusal. + """ yield SimpleNamespace( type="response.created", response=SimpleNamespace(id="conv-safe") ) diff --git a/tests/unit/app/endpoints/test_tools.py b/tests/unit/app/endpoints/test_tools.py index 559a9550c..7138bc7a8 100644 --- a/tests/unit/app/endpoints/test_tools.py +++ b/tests/unit/app/endpoints/test_tools.py @@ -49,7 +49,15 @@ def mock_configuration() -> Configuration: @pytest.fixture def mock_tools_response(mocker: MockerFixture) -> list[MockType]: - """Create mock tools response from LlamaStack client.""" + """Create mock tools response from LlamaStack client. + + Each mock supports mapping-like access (so dict() conversion, iteration, + and item access work) and contains fields: 'identifier', 'description', + 'parameters', 'provider_id', 'toolgroup_id', 'type', and 'metadata'. + + Returns: + list[MockType]: A list with two mock tool objects representing filesystem and git tools. + """ # Create mock tools that behave like dict when converted tool1 = mocker.Mock() tool1.__dict__.update( From 671adb2194f50f1091670a6451439182ac2c8429 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Wed, 17 Dec 2025 09:20:04 +0100 Subject: [PATCH 2/2] Docstrings in app unit tests --- tests/unit/app/test_database.py | 49 +++- tests/unit/app/test_routers.py | 56 ++++- .../models/config/test_dump_configuration.py | 212 ++++++++++++++++++ 3 files changed, 308 insertions(+), 9 deletions(-) diff --git a/tests/unit/app/test_database.py b/tests/unit/app/test_database.py index 89b33a938..02e5301ea 100644 --- a/tests/unit/app/test_database.py +++ b/tests/unit/app/test_database.py @@ -16,7 +16,12 @@ @pytest.fixture(name="reset_database_state") def reset_database_state_fixture() -> Generator: - """Reset global database state before and after tests.""" + """Reset global database state before and after tests. + + Returns: + generator: A fixture generator that yields control to the test and + restores global database state afterwards. + """ original_engine = database.engine original_session_local = database.session_local @@ -33,7 +38,13 @@ def reset_database_state_fixture() -> Generator: @pytest.fixture(name="base_postgres_config") def base_postgres_config_fixture() -> PostgreSQLDatabaseConfiguration: - """Provide base PostgreSQL configuration for tests.""" + """Provide base PostgreSQL configuration for tests. + + Returns: + PostgreSQLDatabaseConfiguration: Configuration with host "localhost", + port 5432, database "testdb", user "testuser", password "testpass", and + namespace "public". + """ return PostgreSQLDatabaseConfiguration( host="localhost", port=5432, @@ -263,7 +274,24 @@ def _setup_common_mocks( mock_logger: MockType, enable_debug: bool = False, ) -> tuple[MockType, MockType]: - """Setup common mocks for initialize_database tests.""" + """Setup common mocks for initialize_database tests. + + Create and configure common mock objects used by + initialize_database tests. + + Parameters: + mocker (MockerFixture): pytest-mock fixture used to create MagicMock instances. + mock_sessionmaker (MockType): Mocked sessionmaker whose return + value will be set to the mocked session-local factory. + mock_logger (MockType): Mock logger whose `isEnabledFor` behavior will be configured. + enable_debug (bool): If True, configures `mock_logger.isEnabledFor` to return True. + + Returns: + tuple[MockType, MockType]: A tuple (mock_engine, + mock_session_local) where `mock_engine` is a mocked SQLAlchemy + Engine and `mock_session_local` is the mocked session-local + factory. + """ mock_engine = mocker.MagicMock(spec=Engine) mock_session_local = mocker.MagicMock() mock_sessionmaker.return_value = mock_session_local @@ -277,7 +305,20 @@ def _verify_common_assertions( mock_engine: MockType, mock_session_local: MockType, ) -> None: - """Verify common assertions for initialize_database tests.""" + """Verify common assertions for initialize_database tests. + + Assert that initialize_database set up the engine and session factory + and that sessionmaker was invoked with the expected arguments. + + Parameters: + mock_sessionmaker (MockType): Mock of the sessionmaker factory; + expected to have been called with autocommit=False, + autoflush=False, bind=mock_engine. + mock_engine (MockType): Mock Engine instance expected to be + assigned to database.engine. + mock_session_local (MockType): Mock session factory expected to be + assigned to database.session_local. + """ mock_sessionmaker.assert_called_once_with( autocommit=False, autoflush=False, bind=mock_engine ) diff --git a/tests/unit/app/test_routers.py b/tests/unit/app/test_routers.py index 1245a07ba..993f212b3 100644 --- a/tests/unit/app/test_routers.py +++ b/tests/unit/app/test_routers.py @@ -30,7 +30,15 @@ class MockFastAPI(FastAPI): """Mock class for FastAPI.""" def __init__(self) -> None: # pylint: disable=super-init-not-called - """Initialize mock class.""" + """Initialize mock class. + + Create a mock FastAPI-like app and initialize its router + registry. + + The instance attribute `routers` is initialized as an empty + list that will store tuples of (router, prefix), where + `prefix` is the route prefix string or `None`. + """ self.routers: list[tuple[Any, Optional[str]]] = [] def include_router( # pylint: disable=too-many-arguments @@ -47,15 +55,45 @@ def include_router( # pylint: disable=too-many-arguments callbacks: Optional[list] = None, generate_unique_id_function: Optional[Callable] = None, ) -> None: - """Register new router.""" + """Register new router. + + Register a router and its mount prefix on the mock FastAPI + app for test inspection. + + Parameters: + router (Any): Router object to register. + prefix (str): Mount prefix to associate with the router. + + Notes: + Accepts additional FastAPI-compatible parameters for + API compatibility but ignores them; only the (router, + prefix) pair is recorded. + """ self.routers.append((router, prefix)) def get_routers(self) -> list[Any]: - """Retrieve all routers defined in mocked REST API.""" + """Retrieve all routers defined in mocked REST API. + + Returns: + routers (list[Any]): List of registered router objects in the order they were added. + """ return [r[0] for r in self.routers] def get_router_prefix(self, router: Any) -> Optional[str]: - """Retrieve router prefix configured for mocked REST API.""" + """Retrieve router prefix configured for mocked REST API. + + Get the prefix associated with a registered router in the mock FastAPI. + + Parameters: + router (Any): Router object to look up. + + Returns: + Optional[str]: The prefix string for the router, or `None` if the + router was registered without a prefix. + + Raises: + IndexError: If the router is not registered in the mock app. + """ return list(filter(lambda r: r[0] == router, self.routers))[0][1] @@ -87,7 +125,15 @@ def test_include_routers() -> None: def test_check_prefixes() -> None: - """Test the router prefixes.""" + """Test the router prefixes. + + Verify that include_routers registers the expected routers with their configured URL prefixes. + + Asserts that 16 routers are registered on a MockFastAPI instance and that + each router's prefix matches the expected value (e.g., root, health, + authorized, metrics use an empty prefix; most API routers use "/v1"; + conversations_v2 uses "/v2"). + """ app = MockFastAPI() include_routers(app) diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index 8bcd134f7..456d38c52 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -1,5 +1,7 @@ """Unit tests checking ability to dump configuration.""" +# pylint: disable=too-many-lines + import json from pathlib import Path @@ -512,6 +514,216 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None: } +def test_dump_configuration_with_quota_limiters_different_values( + tmp_path: Path, +) -> None: + """ + Test that the Configuration object can be serialized to a JSON file and + that the resulting file contains all expected sections and values. + + Please note that redaction process is not in place. + """ + cfg = Configuration( + name="test_name", + service=ServiceConfiguration( + tls_config=TLSConfiguration( + tls_certificate_path=Path("tests/configuration/server.crt"), + tls_key_path=Path("tests/configuration/server.key"), + tls_key_password=Path("tests/configuration/password"), + ), + cors=CORSConfiguration( + allow_origins=["foo_origin", "bar_origin", "baz_origin"], + allow_credentials=False, + allow_methods=["foo_method", "bar_method", "baz_method"], + allow_headers=["foo_header", "bar_header", "baz_header"], + ), + ), + llama_stack=LlamaStackConfiguration( + use_as_library_client=True, + library_client_config_path="tests/configuration/run.yaml", + api_key=SecretStr("whatever"), + ), + user_data_collection=UserDataCollection( + feedback_enabled=False, feedback_storage=None + ), + database=DatabaseConfiguration( + sqlite=None, + postgres=PostgreSQLDatabaseConfiguration( + db="lightspeed_stack", + user="ls_user", + password=SecretStr("ls_password"), + port=5432, + ca_cert_path=None, + ssl_mode="require", + gss_encmode="disable", + ), + ), + mcp_servers=[], + customization=None, + inference=InferenceConfiguration( + default_provider="default_provider", + default_model="default_model", + ), + quota_handlers=QuotaHandlersConfiguration( + limiters=[ + QuotaLimiterConfiguration( + type="user_limiter", + name="user_monthly_limits", + initial_quota=1, + quota_increase=10, + period="2 seconds", + ), + QuotaLimiterConfiguration( + type="cluster_limiter", + name="cluster_monthly_limits", + initial_quota=2, + quota_increase=20, + period="1 month", + ), + ], + scheduler=QuotaSchedulerConfiguration( + period=10, + database_reconnection_count=123, + database_reconnection_delay=456, + ), + enable_token_history=True, + ), + ) + assert cfg is not None + dump_file = tmp_path / "test.json" + cfg.dump(dump_file) + + with open(dump_file, "r", encoding="utf-8") as fin: + content = json.load(fin) + # content should be loaded + assert content is not None + + # all sections must exists + assert "name" in content + assert "service" in content + assert "llama_stack" in content + assert "user_data_collection" in content + assert "mcp_servers" in content + assert "authentication" in content + assert "authorization" in content + assert "customization" in content + assert "inference" in content + assert "database" in content + assert "byok_rag" in content + assert "quota_handlers" in content + + # check the whole deserialized JSON file content + assert content == { + "name": "test_name", + "service": { + "host": "localhost", + "port": 8080, + "auth_enabled": False, + "workers": 1, + "color_log": True, + "access_log": True, + "tls_config": { + "tls_certificate_path": "tests/configuration/server.crt", + "tls_key_password": "tests/configuration/password", + "tls_key_path": "tests/configuration/server.key", + }, + "cors": { + "allow_credentials": False, + "allow_headers": [ + "foo_header", + "bar_header", + "baz_header", + ], + "allow_methods": [ + "foo_method", + "bar_method", + "baz_method", + ], + "allow_origins": [ + "foo_origin", + "bar_origin", + "baz_origin", + ], + }, + }, + "llama_stack": { + "url": None, + "use_as_library_client": True, + "api_key": "**********", + "library_client_config_path": "tests/configuration/run.yaml", + }, + "user_data_collection": { + "feedback_enabled": False, + "feedback_storage": None, + "transcripts_enabled": False, + "transcripts_storage": None, + }, + "mcp_servers": [], + "authentication": { + "module": "noop", + "skip_tls_verification": False, + "k8s_ca_cert_path": None, + "k8s_cluster_api": None, + "jwk_config": None, + "api_key_config": None, + "rh_identity_config": None, + }, + "customization": None, + "inference": { + "default_provider": "default_provider", + "default_model": "default_model", + }, + "database": { + "sqlite": None, + "postgres": { + "host": "localhost", + "port": 5432, + "db": "lightspeed_stack", + "user": "ls_user", + "password": "**********", + "ssl_mode": "require", + "gss_encmode": "disable", + "namespace": "public", + "ca_cert_path": None, + }, + }, + "authorization": None, + "conversation_cache": { + "memory": None, + "postgres": None, + "sqlite": None, + "type": None, + }, + "byok_rag": [], + "quota_handlers": { + "sqlite": None, + "postgres": None, + "limiters": [ + { + "initial_quota": 1, + "name": "user_monthly_limits", + "period": "2 seconds", + "quota_increase": 10, + "type": "user_limiter", + }, + { + "initial_quota": 2, + "name": "cluster_monthly_limits", + "period": "1 month", + "quota_increase": 20, + "type": "cluster_limiter", + }, + ], + "scheduler": { + "period": 10, + "database_reconnection_count": 123, + "database_reconnection_delay": 456, + }, + "enable_token_history": True, + }, + } + + def test_dump_configuration_byok(tmp_path: Path) -> None: """ Test that the Configuration object can be serialized to a JSON file and