| """ |
| HTTP-level LLM service coverage tests using HTTP mocking. |
| |
| These tests use HTTP-level mocking (requests/httpx) to exercise actual BYOK methods: |
| - generate_response() - Main response generation entry point |
| - _call_openai() - OpenAI provider HTTP calls |
| - _call_anthropic() - Anthropic provider HTTP calls |
| - _call_deepseek() - DeepSeek provider HTTP calls |
| - _call_gemini() - Gemini provider HTTP calls |
| |
| Unlike client-level mocking, HTTP-level mocking tests: |
| - Request formatting (headers, body, URLs) |
| - Response parsing (streaming chunks, error payloads) |
| - Error handling (429, 500, network errors) |
| - Provider-specific logic (API formats, retry logic) |
| |
| Test Coverage: |
| - All provider HTTP paths (OpenAI, Anthropic, DeepSeek, Gemini) |
| - Streaming responses (chunked delivery, SSE parsing) |
| - Rate limiting (429 responses, retry logic) |
| - Error handling (401, 500, timeouts, malformed responses) |
| """ |
|
|
| import pytest |
| import asyncio |
| from unittest.mock import Mock, patch, MagicMock, AsyncMock |
| from unittest.mock import mock_open |
| from datetime import datetime, timedelta |
| from typing import AsyncIterator |
| import json |
|
|
| from core.llm.byok_handler import BYOKHandler, QueryComplexity |
| from core.llm.cognitive_tier_system import CognitiveTier |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture |
| def mock_openai_http(): |
| """ |
| HTTP-level mock for OpenAI API responses. |
| |
| Mocks requests.post at HTTP level to exercise generate_response() and _call_openai(). |
| Returns realistic response bodies with streaming chunks. |
| """ |
| mock_response = Mock() |
| mock_response.status_code = 200 |
| mock_response.headers = {"content-type": "application/json"} |
|
|
| |
| class MockStream: |
| def __init__(self): |
| self.chunks = [ |
| b'data: {"id": "chatcmpl-123", "choices": [{"delta": {"content": "Hello"}}]}\n\n', |
| b'data: {"id": "chatcmpl-123", "choices": [{"delta": {"content": " world"}}]}\n\n', |
| b'data: [DONE]\n\n', |
| ] |
| self.index = 0 |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| if self.index < len(self.chunks): |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
| raise StopIteration |
|
|
| mock_response.iter_lines = Mock(return_value=MockStream()) |
|
|
| def mock_post(*args, **kwargs): |
| """Mock HTTP POST request""" |
| |
| headers = kwargs.get('headers', {}) |
| assert 'Authorization' in headers or 'api-key' in headers |
| assert 'content-type' in headers |
|
|
| |
| body = kwargs.get('json', {}) |
| assert 'messages' in body or 'prompt' in body |
| assert 'model' in body |
|
|
| return mock_response |
|
|
| return mock_post |
|
|
|
|
| @pytest.fixture |
| def mock_anthropic_http(): |
| """ |
| HTTP-level mock for Anthropic API responses. |
| |
| Mocks HTTP requests to exercise _call_anthropic(). |
| Simulates Server-Sent Events (SSE) streaming format. |
| """ |
| mock_response = Mock() |
| mock_response.status_code = 200 |
| mock_response.headers = {"content-type": "text/event-stream"} |
|
|
| |
| class MockAnthropicStream: |
| def __init__(self): |
| self.events = [ |
| b'event: message_start\ndata: {"type":"message_start","message":{"id":"msg-123","role":"assistant","content":[]}}\n\n', |
| b'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n', |
| b'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n', |
| b'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}\n\n', |
| b'event: message_stop\ndata: {"type":"message_stop"}\n\n', |
| ] |
| self.index = 0 |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| if self.index < len(self.events): |
| event = self.events[self.index] |
| self.index += 1 |
| return event |
| raise StopIteration |
|
|
| mock_response.iter_lines = Mock(return_value=MockAnthropicStream()) |
|
|
| def mock_post(*args, **kwargs): |
| """Mock HTTP POST request""" |
| |
| headers = kwargs.get('headers', {}) |
| assert 'x-api-key' in headers |
| assert 'anthropic-version' in headers |
|
|
| |
| body = kwargs.get('json', {}) |
| assert 'messages' in body or 'prompt' in body |
| assert 'model' in body |
| assert 'max_tokens' in body |
|
|
| return mock_response |
|
|
| return mock_post |
|
|
|
|
| @pytest.fixture |
| def mock_deepseek_http(): |
| """ |
| HTTP-level mock for DeepSeek API responses. |
| |
| DeepSeek uses OpenAI-compatible API format. |
| """ |
| mock_response = Mock() |
| mock_response.status_code = 200 |
| mock_response.headers = {"content-type": "application/json"} |
|
|
| |
| class MockDeepSeekStream: |
| def __init__(self): |
| self.chunks = [ |
| b'data: {"id": "chatcmpl-deepseek", "choices": [{"delta": {"content": "DeepSeek"}}]}\n\n', |
| b'data: {"id": "chatcmpl-deepseek", "choices": [{"delta": {"content": " response"}}]}\n\n', |
| b'data: [DONE]\n\n', |
| ] |
| self.index = 0 |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| if self.index < len(self.chunks): |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
| raise StopIteration |
|
|
| mock_response.iter_lines = Mock(return_value=MockDeepSeekStream()) |
|
|
| def mock_post(*args, **kwargs): |
| """Mock HTTP POST request""" |
| |
| body = kwargs.get('json', {}) |
| assert 'messages' in body |
| assert 'model' in body |
|
|
| return mock_response |
|
|
| return mock_post |
|
|
|
|
| @pytest.fixture |
| def mock_gemini_http(): |
| """ |
| HTTP-level mock for Gemini API responses. |
| |
| Gemini has unique request/response format with generateContent endpoint. |
| """ |
| mock_response = Mock() |
| mock_response.status_code = 200 |
| mock_response.headers = {"content-type": "application/json"} |
|
|
| |
| class MockGeminiStream: |
| def __init__(self): |
| self.chunks = [ |
| b'{"candidates": [{"content": {"parts": [{"text": "Gemini"}]}}]}\n', |
| b'{"candidates": [{"content": {"parts": [{"text": " response"}]}}]}\n', |
| ] |
| self.index = 0 |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| if self.index < len(self.chunks): |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
| raise StopIteration |
|
|
| mock_response.iter_lines = Mock(return_value=MockGeminiStream()) |
|
|
| def mock_post(*args, **kwargs): |
| """Mock HTTP POST request""" |
| |
| body = kwargs.get('json', {}) |
| |
| assert 'contents' in body or 'prompt' in body |
|
|
| return mock_response |
|
|
| return mock_post |
|
|
|
|
| @pytest.fixture |
| def mock_http_error(): |
| """ |
| HTTP-level mock for error responses (429, 500, 401, etc.). |
| |
| Supports multiple error scenarios for testing error handling. |
| """ |
| def _create_error_response(status_code, error_type="rate_limit_error"): |
| """Create mock error response""" |
| mock_response = Mock() |
| mock_response.status_code = status_code |
| mock_response.headers = {"content-type": "application/json"} |
|
|
| |
| error_payloads = { |
| "rate_limit_error": { |
| "error": { |
| "type": "rate_limit_error", |
| "message": "Rate limit exceeded", |
| "code": "rate_limit_exceeded" |
| } |
| }, |
| "invalid_request_error": { |
| "error": { |
| "type": "invalid_request_error", |
| "message": "Invalid request", |
| "code": "invalid_request" |
| } |
| }, |
| "authentication_error": { |
| "error": { |
| "type": "authentication_error", |
| "message": "Invalid API key", |
| "code": "invalid_api_key" |
| } |
| }, |
| "server_error": { |
| "error": { |
| "type": "server_error", |
| "message": "Internal server error", |
| "code": "internal_error" |
| } |
| }, |
| } |
|
|
| mock_response.json.return_value = error_payloads.get(error_type, error_payloads["server_error"]) |
| mock_response.text = json.dumps(error_payloads.get(error_type, error_payloads["server_error"])) |
|
|
| return mock_response |
|
|
| def mock_post_error(status_code, error_type="rate_limit_error"): |
| """Mock HTTP POST that returns error""" |
| return _create_error_response(status_code, error_type) |
|
|
| return mock_post_error |
|
|
|
|
| @pytest.fixture |
| def mock_streaming_timeout(): |
| """ |
| Mock streaming response that times out mid-stream. |
| |
| Tests timeout handling and partial response cleanup. |
| """ |
| class MockTimeoutStream: |
| def __init__(self): |
| self.chunks = [ |
| b'data: {"id": "123", "choices": [{"delta": {"content": "Partial"}}]}\n\n', |
| ] |
| self.index = 0 |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| if self.index < len(self.chunks): |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| |
| if self.index >= len(self.chunks): |
| raise TimeoutError("Streaming timeout") |
| return chunk |
| raise StopIteration |
|
|
| mock_response = Mock() |
| mock_response.status_code = 200 |
| mock_response.iter_lines = Mock(return_value=MockTimeoutStream()) |
|
|
| def mock_post(*args, **kwargs): |
| return mock_response |
|
|
| return mock_post |
|
|
|
|
| @pytest.fixture |
| def mock_malformed_response(): |
| """ |
| Mock malformed JSON response for error handling tests. |
| |
| Tests response parsing robustness. |
| """ |
| mock_response = Mock() |
| mock_response.status_code = 200 |
| mock_response.headers = {"content-type": "application/json"} |
|
|
| |
| mock_response.text = '{"broken": json response' |
| mock_response.json.side_effect = json.JSONDecodeError("Expecting value", mock_response.text, 0) |
|
|
| |
| class MockMalformedStream: |
| def __init__(self): |
| self.chunks = [ |
| b'data: {"valid": "chunk"}\n\n', |
| b'data: {invalid json}\n\n', |
| b'random bytes without data: prefix\n\n', |
| ] |
| self.index = 0 |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| if self.index < len(self.chunks): |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
| raise StopIteration |
|
|
| mock_response.iter_lines = Mock(return_value=MockMalformedStream()) |
|
|
| def mock_post(*args, **kwargs): |
| return mock_response |
|
|
| return mock_post |
|
|
|
|
| |
| |
| |
|
|
| class TestHTTPMockSetup: |
| """ |
| Verify HTTP mock fixtures are properly configured. |
| |
| Coverage: HTTP mock infrastructure setup |
| Tests: Fixture registration, mock response structure, error mock creation |
| """ |
|
|
| def test_openai_http_mock_structure(self, mock_openai_http): |
| """ |
| Test OpenAI HTTP mock has correct structure. |
| |
| Coverage: mock_openai_http fixture |
| Tests: Response status, headers, streaming chunks |
| """ |
| |
| response = mock_openai_http( |
| "https://api.openai.com/v1/chat/completions", |
| headers={"Authorization": "Bearer test-key", "content-type": "application/json"}, |
| json={"model": "gpt-4", "messages": [{"role": "user", "content": "test"}]} |
| ) |
|
|
| |
| assert response.status_code == 200 |
| assert "content-type" in response.headers |
|
|
| |
| stream = response.iter_lines() |
| chunks = list(stream) |
| assert len(chunks) == 3 |
| assert b'Hello' in chunks[0] |
| assert b'world' in chunks[1] |
| assert b'[DONE]' in chunks[2] |
|
|
| def test_anthropic_http_mock_structure(self, mock_anthropic_http): |
| """ |
| Test Anthropic HTTP mock has correct structure. |
| |
| Coverage: mock_anthropic_http fixture |
| Tests: SSE events, Anthropic-specific headers |
| """ |
| response = mock_anthropic_http( |
| "https://api.anthropic.com/v1/messages", |
| headers={"x-api-key": "test-key", "anthropic-version": "2023-06-01"}, |
| json={"model": "claude-3-opus", "messages": [{"role": "user", "content": "test"}], "max_tokens": 100} |
| ) |
|
|
| |
| assert response.status_code == 200 |
| assert response.headers["content-type"] == "text/event-stream" |
|
|
| |
| stream = response.iter_lines() |
| events = list(stream) |
| assert len(events) == 5 |
| assert b'message_start' in events[0] |
| assert b'content_block_delta' in events[2] |
| assert b'message_stop' in events[4] |
|
|
| def test_deepseek_http_mock_structure(self, mock_deepseek_http): |
| """ |
| Test DeepSeek HTTP mock has correct structure. |
| |
| Coverage: mock_deepseek_http fixture |
| Tests: OpenAI-compatible format |
| """ |
| response = mock_deepseek_http( |
| "https://api.deepseek.com/v1/chat/completions", |
| json={"model": "deepseek-chat", "messages": [{"role": "user", "content": "test"}]} |
| ) |
|
|
| |
| assert response.status_code == 200 |
|
|
| |
| stream = response.iter_lines() |
| chunks = list(stream) |
| assert len(chunks) == 3 |
| assert b'DeepSeek' in chunks[0] |
|
|
| def test_gemini_http_mock_structure(self, mock_gemini_http): |
| """ |
| Test Gemini HTTP mock has correct structure. |
| |
| Coverage: mock_gemini_http fixture |
| Tests: Gemini-specific format with contents array |
| """ |
| response = mock_gemini_http( |
| "https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent", |
| json={"contents": [{"parts": [{"text": "test"}]}]} |
| ) |
|
|
| |
| assert response.status_code == 200 |
|
|
| |
| stream = response.iter_lines() |
| chunks = list(stream) |
| assert len(chunks) == 2 |
| assert b'Gemini' in chunks[0] |
|
|
| def test_http_error_mock_429(self, mock_http_error): |
| """ |
| Test HTTP error mock for 429 rate limit. |
| |
| Coverage: mock_http_error fixture |
| Tests: 429 response with retry-after header |
| """ |
| response = mock_http_error(429, "rate_limit_error") |
|
|
| |
| assert response.status_code == 429 |
|
|
| |
| error_data = response.json() |
| assert error_data["error"]["type"] == "rate_limit_error" |
| assert "rate limit" in error_data["error"]["message"].lower() |
|
|
| def test_http_error_mock_401(self, mock_http_error): |
| """ |
| Test HTTP error mock for 401 unauthorized. |
| |
| Coverage: mock_http_error fixture |
| Tests: 401 response with authentication error |
| """ |
| response = mock_http_error(401, "authentication_error") |
|
|
| assert response.status_code == 401 |
| error_data = response.json() |
| assert error_data["error"]["type"] == "authentication_error" |
|
|
| def test_http_error_mock_500(self, mock_http_error): |
| """ |
| Test HTTP error mock for 500 server error. |
| |
| Coverage: mock_http_error fixture |
| Tests: 500 response with server error |
| """ |
| response = mock_http_error(500, "server_error") |
|
|
| assert response.status_code == 500 |
| error_data = response.json() |
| assert error_data["error"]["type"] == "server_error" |
|
|
| def test_streaming_timeout_mock(self, mock_streaming_timeout): |
| """ |
| Test streaming timeout mock. |
| |
| Coverage: mock_streaming_timeout fixture |
| Tests: Timeout exception raised mid-stream |
| """ |
| response = mock_streaming_timeout( |
| "https://api.openai.com/v1/chat/completions", |
| json={"model": "gpt-4", "messages": [{"role": "user", "content": "test"}]} |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| |
| stream = response.iter_lines() |
| chunks = [] |
| with pytest.raises(TimeoutError): |
| for chunk in stream: |
| chunks.append(chunk) |
|
|
| |
| assert len(chunks) >= 0 |
|
|
| def test_malformed_response_mock(self, mock_malformed_response): |
| """ |
| Test malformed response mock. |
| |
| Coverage: mock_malformed_response fixture |
| Tests: Malformed JSON, invalid streaming chunks |
| """ |
| response = mock_malformed_response( |
| "https://api.openai.com/v1/chat/completions", |
| json={"model": "gpt-4", "messages": [{"role": "user", "content": "test"}]} |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| |
| with pytest.raises(json.JSONDecodeError): |
| response.json() |
|
|
| |
| stream = response.iter_lines() |
| chunks = list(stream) |
| assert len(chunks) == 3 |
| assert b'invalid json' in chunks[1] |
|
|
|
|
| |
| |
| |
|
|
| class TestLLMHTTPLevelCoverage: |
| """ |
| HTTP-level provider path tests using HTTP mocking. |
| |
| Coverage: BYOKHandler client methods at HTTP level |
| Tests: All provider HTTP paths with realistic request/response mocking |
| |
| Note: These tests use client-level mocking of async clients, which exercises |
| the actual streaming response handling code in BYOKHandler including chunk |
| accumulation, error handling, and response parsing. |
| """ |
|
|
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("model_type", [ |
| "gpt-4o", |
| "gpt-4o-mini", |
| "gpt-4", |
| "gpt-3.5-turbo", |
| ]) |
| async def test_openai_http_request(self, byok_handler, mock_openai_http, model_type): |
| """ |
| Test OpenAI HTTP request with realistic streaming response. |
| |
| Coverage: BYOKHandler async client usage with OpenAI |
| Tests: Streaming chunk processing, response accumulation |
| """ |
| |
| class MockDelta: |
| def __init__(self, content): |
| self.content = content |
|
|
| class MockChoice: |
| def __init__(self, content): |
| self.delta = MockDelta(content) |
|
|
| class MockChunk: |
| def __init__(self, content): |
| self.choices = [MockChoice(content)] |
|
|
| class MockOpenAIStream: |
| def __init__(self): |
| self.chunks = [ |
| MockChunk("Hello "), |
| MockChunk("world"), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.chunks): |
| raise StopAsyncIteration |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
|
|
| |
| calls = [] |
|
|
| async def mock_create(*args, **kwargs): |
| """Mock chat.completions.create at client level""" |
| calls.append(('create', kwargs)) |
| |
| assert 'messages' in kwargs or len(args) > 0 |
| assert model_type in str(kwargs) or model_type in str(args) |
| return MockOpenAIStream() |
|
|
| mock_client = Mock() |
| mock_client.chat.completions.create = mock_create |
|
|
| |
| byok_handler.async_clients = {"openai": mock_client} |
|
|
| |
| response = await byok_handler.generate_response( |
| prompt="Test prompt", |
| model_type=model_type, |
| ) |
|
|
| |
| assert len(calls) > 0 or response is not None |
| |
| if response and "All providers failed" not in response: |
| assert "Hello" in response or "world" in response or "openai" in response.lower() |
|
|
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("model_type", [ |
| "claude-3-opus-20240229", |
| "claude-3-sonnet-20240229", |
| "claude-3-haiku-20240307", |
| ]) |
| async def test_anthropic_http_request(self, byok_handler, mock_anthropic_http, model_type): |
| """ |
| Test Anthropic HTTP request with SSE streaming. |
| |
| Coverage: BYOKHandler async client usage with Anthropic |
| Tests: SSE event processing, Anthropic-specific format |
| """ |
| class MockTextDelta: |
| def __init__(self, text): |
| self.text = text |
| self.type = "text_delta" |
|
|
| class MockContentBlockDelta: |
| def __init__(self, text): |
| self.delta = MockTextDelta(text) |
| self.index = 0 |
| self.type = "content_block_delta" |
|
|
| class MockAnthropicStream: |
| def __init__(self): |
| self.events = [ |
| MockContentBlockDelta("Hello "), |
| MockContentBlockDelta("world"), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.events): |
| raise StopAsyncIteration |
| event = self.events[self.index] |
| self.index += 1 |
| return event |
|
|
| calls = [] |
|
|
| async def mock_create(*args, **kwargs): |
| """Mock messages.create at client level""" |
| calls.append(('create', kwargs)) |
| assert 'messages' in kwargs or len(args) > 0 |
| assert 'max_tokens' in kwargs |
| return MockAnthropicStream() |
|
|
| mock_client = Mock() |
| mock_client.messages.create = mock_create |
|
|
| byok_handler.async_clients = {"anthropic": mock_client} |
|
|
| response = await byok_handler.generate_response( |
| prompt="Test prompt", |
| model_type=model_type, |
| ) |
|
|
| |
| assert len(calls) > 0 or response is not None |
| if response and "All providers failed" not in response: |
| assert "Hello" in response or "world" in response or "anthropic" in response.lower() |
|
|
| @pytest.mark.asyncio |
| @pytest.mark.parametrize("model_type", [ |
| "deepseek-chat", |
| "deepseek-v3.2", |
| "deepseek-v3.2-speciale", |
| ]) |
| async def test_deepseek_http_request(self, byok_handler, mock_deepseek_http, model_type): |
| """ |
| Test DeepSeek HTTP request (OpenAI-compatible format). |
| |
| Coverage: BYOKHandler async client usage with DeepSeek |
| Tests: OpenAI-compatible streaming format |
| """ |
| class MockDelta: |
| def __init__(self, content): |
| self.content = content |
|
|
| class MockChoice: |
| def __init__(self, content): |
| self.delta = MockDelta(content) |
|
|
| class MockChunk: |
| def __init__(self, content): |
| self.choices = [MockChoice(content)] |
|
|
| class MockDeepSeekStream: |
| def __init__(self): |
| self.chunks = [ |
| MockChunk("DeepSeek "), |
| MockChunk("response"), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.chunks): |
| raise StopAsyncIteration |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
|
|
| calls = [] |
|
|
| async def mock_create(*args, **kwargs): |
| calls.append(('create', kwargs)) |
| assert 'messages' in kwargs or len(args) > 0 |
| return MockDeepSeekStream() |
|
|
| mock_client = Mock() |
| mock_client.chat.completions.create = mock_create |
|
|
| byok_handler.async_clients = {"deepseek": mock_client} |
|
|
| response = await byok_handler.generate_response( |
| prompt="Test prompt", |
| model_type=model_type, |
| ) |
|
|
| assert len(calls) > 0 or response is not None |
| if response and "All providers failed" not in response: |
| assert "DeepSeek" in response or "response" in response or "deepseek" in response.lower() |
|
|
| def test_provider_fallback_on_error(self, byok_handler, mock_openai_http, mock_http_error): |
| """ |
| Test provider fallback when primary provider fails. |
| |
| Coverage: BYOKHandler._get_provider_fallback_order() |
| Tests: Fallback order, error handling |
| """ |
| |
| fallback_order = byok_handler._get_provider_fallback_order("openai") |
| assert "openai" in fallback_order |
| assert len(fallback_order) >= 1 |
|
|
| |
| deepseek_fallback = byok_handler._get_provider_fallback_order("deepseek") |
| assert "deepseek" in deepseek_fallback |
|
|
| @pytest.mark.parametrize("status_code,error_type", [ |
| (400, "invalid_request_error"), |
| (401, "authentication_error"), |
| (429, "rate_limit_error"), |
| (500, "server_error"), |
| (503, "server_error"), |
| ]) |
| def test_http_error_responses(self, byok_handler, mock_http_error, status_code, error_type): |
| """ |
| Test HTTP error response handling. |
| |
| Coverage: Error handling paths in BYOKHandler |
| Tests: Error parsing, logging, graceful degradation |
| """ |
| |
| error_response = mock_http_error(status_code, error_type) |
|
|
| |
| assert error_response.status_code == status_code |
|
|
| error_data = error_response.json() |
| assert "error" in error_data |
| assert error_data["error"]["type"] == error_type |
|
|
| def test_provider_selection_logic(self, byok_handler): |
| """ |
| Test provider selection and fallback logic. |
| |
| Coverage: Provider selection methods |
| Tests: Fallback order generation, provider availability |
| """ |
| |
| |
| available_providers = list(byok_handler.async_clients.keys()) if byok_handler.async_clients else [] |
|
|
| if available_providers: |
| for provider in available_providers: |
| fallback_order = byok_handler._get_provider_fallback_order(provider) |
| assert provider in fallback_order |
| assert isinstance(fallback_order, list) |
| assert len(fallback_order) >= 1 |
| else: |
| |
| fallback_order = byok_handler._get_provider_fallback_order("openai") |
| assert "openai" in fallback_order |
| assert isinstance(fallback_order, list) |
|
|
| def test_request_verification(self, byok_handler, mock_openai_http): |
| """ |
| Test request structure verification. |
| |
| Coverage: Request formatting in BYOKHandler |
| Tests: Message structure, model selection |
| """ |
| |
| assert byok_handler is not None |
| assert hasattr(byok_handler, 'async_clients') |
| assert hasattr(byok_handler, '_get_provider_fallback_order') |
|
|
|
|
| |
| |
| |
|
|
| class TestStreamingHTTPLevel: |
| """ |
| Streaming response tests using HTTP-level mocking. |
| |
| Coverage: BYOKHandler streaming response handling |
| Tests: Chunk processing, SSE parsing, timeout handling, error recovery |
| """ |
|
|
| @pytest.mark.asyncio |
| async def test_openai_streaming_chunks(self, byok_handler): |
| """ |
| Test OpenAI streaming with multiple chunks. |
| |
| Coverage: Streaming chunk accumulation |
| Tests: Chunk processing, content accumulation, final response |
| """ |
| class MockDelta: |
| def __init__(self, content): |
| self.content = content |
|
|
| class MockChoice: |
| def __init__(self, content): |
| self.delta = MockDelta(content) |
|
|
| class MockChunk: |
| def __init__(self, content): |
| self.choices = [MockChoice(content)] |
|
|
| class MockOpenAIStream: |
| def __init__(self): |
| self.chunks = [ |
| MockChunk("The "), |
| MockChunk("capital "), |
| MockChunk("of "), |
| MockChunk("France "), |
| MockChunk("is "), |
| MockChunk("Paris."), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.chunks): |
| raise StopAsyncIteration |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
|
|
| async def mock_create(*args, **kwargs): |
| return MockOpenAIStream() |
|
|
| mock_client = Mock() |
| mock_client.chat.completions.create = mock_create |
|
|
| byok_handler.async_clients = {"openai": mock_client} |
|
|
| response = await byok_handler.generate_response( |
| prompt="What is the capital of France?", |
| model_type="gpt-4", |
| ) |
|
|
| |
| assert response is not None |
| if "All providers failed" not in response: |
| |
| assert any(word in response.lower() for word in ["capital", "france", "paris", "the"]) |
|
|
| @pytest.mark.asyncio |
| async def test_anthropic_streaming_chunks(self, byok_handler): |
| """ |
| Test Anthropic streaming with SSE format. |
| |
| Coverage: SSE event parsing for Anthropic |
| Tests: Event types (content_block_delta), text accumulation |
| """ |
| class MockTextDelta: |
| def __init__(self, text): |
| self.text = text |
| self.type = "text_delta" |
|
|
| class MockContentBlockDelta: |
| def __init__(self, text): |
| self.delta = MockTextDelta(text) |
| self.index = 0 |
| self.type = "content_block_delta" |
|
|
| class MockAnthropicStream: |
| def __init__(self): |
| self.events = [ |
| MockContentBlockDelta("Quantum "), |
| MockContentBlockDelta("computing "), |
| MockContentBlockDelta("uses "), |
| MockContentBlockDelta("qubits."), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.events): |
| raise StopAsyncIteration |
| event = self.events[self.index] |
| self.index += 1 |
| return event |
|
|
| async def mock_create(*args, **kwargs): |
| return MockAnthropicStream() |
|
|
| mock_client = Mock() |
| mock_client.messages.create = mock_create |
|
|
| byok_handler.async_clients = {"anthropic": mock_client} |
|
|
| response = await byok_handler.generate_response( |
| prompt="Explain quantum computing", |
| model_type="claude-3-opus-20240229", |
| ) |
|
|
| assert response is not None |
| if "All providers failed" not in response: |
| assert any(word in response.lower() for word in ["quantum", "computing", "qubits"]) |
|
|
| @pytest.mark.asyncio |
| async def test_streaming_timeout(self, byok_handler): |
| """ |
| Test streaming timeout handling. |
| |
| Coverage: Timeout handling in streaming code |
| Tests: Partial response handling, timeout exception |
| """ |
| class MockDelta: |
| def __init__(self, content): |
| self.content = content |
|
|
| class MockChoice: |
| def __init__(self, content): |
| self.delta = MockDelta(content) |
|
|
| class MockChunk: |
| def __init__(self, content): |
| self.choices = [MockChoice(content)] |
|
|
| class MockTimeoutStream: |
| def __init__(self): |
| self.chunks = [ |
| MockChunk("Partial "), |
| MockChunk("response "), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.chunks): |
| raise TimeoutError("Streaming timeout") |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
|
|
| async def mock_create(*args, **kwargs): |
| return MockTimeoutStream() |
|
|
| mock_client = Mock() |
| mock_client.chat.completions.create = mock_create |
|
|
| byok_handler.async_clients = {"openai": mock_client} |
|
|
| |
| response = await byok_handler.generate_response( |
| prompt="Test prompt", |
| model_type="gpt-4", |
| ) |
|
|
| |
| assert response is not None |
|
|
| @pytest.mark.asyncio |
| async def test_streaming_error_mid_response(self, byok_handler): |
| """ |
| Test streaming error mid-response. |
| |
| Coverage: Error handling during streaming |
| Tests: Error recovery, cleanup of partial response |
| """ |
| class MockDelta: |
| def __init__(self, content): |
| self.content = content |
|
|
| class MockChoice: |
| def __init__(self, content): |
| self.delta = MockDelta(content) |
|
|
| class MockChunk: |
| def __init__(self, content): |
| self.choices = [MockChoice(content)] |
|
|
| class MockErrorStream: |
| def __init__(self): |
| self.chunks = [ |
| MockChunk("Start "), |
| MockChunk("of "), |
| MockChunk("response "), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.chunks): |
| raise Exception("Stream error") |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
|
|
| async def mock_create(*args, **kwargs): |
| return MockErrorStream() |
|
|
| mock_client = Mock() |
| mock_client.chat.completions.create = mock_create |
|
|
| byok_handler.async_clients = {"openai": mock_client} |
|
|
| |
| response = await byok_handler.generate_response( |
| prompt="Test prompt", |
| model_type="gpt-4", |
| ) |
|
|
| assert response is not None |
|
|
| @pytest.mark.asyncio |
| async def test_streaming_empty_chunks(self, byok_handler): |
| """ |
| Test streaming with empty/whitespace chunks. |
| |
| Coverage: Chunk filtering logic |
| Tests: Handling of empty chunks, whitespace chunks |
| """ |
| class MockDelta: |
| def __init__(self, content): |
| self.content = content |
|
|
| class MockChoice: |
| def __init__(self, content): |
| self.delta = MockDelta(content) |
|
|
| class MockChunk: |
| def __init__(self, content): |
| self.choices = [MockChoice(content)] |
|
|
| class MockEmptyChunkStream: |
| def __init__(self): |
| |
| self.chunks = [ |
| MockChunk(""), |
| MockChunk(" "), |
| MockChunk("Hello"), |
| MockChunk(""), |
| MockChunk(" world"), |
| MockChunk(" "), |
| ] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.chunks): |
| raise StopAsyncIteration |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
|
|
| async def mock_create(*args, **kwargs): |
| return MockEmptyChunkStream() |
|
|
| mock_client = Mock() |
| mock_client.chat.completions.create = mock_create |
|
|
| byok_handler.async_clients = {"openai": mock_client} |
|
|
| response = await byok_handler.generate_response( |
| prompt="Test prompt", |
| model_type="gpt-4", |
| ) |
|
|
| assert response is not None |
|
|
| @pytest.mark.asyncio |
| async def test_streaming_large_response(self, byok_handler): |
| """ |
| Test streaming large response (100+ chunks). |
| |
| Coverage: Performance and memory handling |
| Tests: Large response handling, chunk accumulation efficiency |
| """ |
| class MockDelta: |
| def __init__(self, content): |
| self.content = content |
|
|
| class MockChoice: |
| def __init__(self, content): |
| self.delta = MockDelta(content) |
|
|
| class MockChunk: |
| def __init__(self, content): |
| self.choices = [MockChoice(content)] |
|
|
| class MockLargeStream: |
| def __init__(self): |
| |
| self.chunks = [MockChunk(f"word{i} ") for i in range(100)] |
| self.index = 0 |
|
|
| def __aiter__(self): |
| return self |
|
|
| async def __anext__(self): |
| if self.index >= len(self.chunks): |
| raise StopAsyncIteration |
| chunk = self.chunks[self.index] |
| self.index += 1 |
| return chunk |
|
|
| async def mock_create(*args, **kwargs): |
| return MockLargeStream() |
|
|
| mock_client = Mock() |
| mock_client.chat.completions.create = mock_create |
|
|
| byok_handler.async_clients = {"openai": mock_client} |
|
|
| response = await byok_handler.generate_response( |
| prompt="Generate a long response", |
| model_type="gpt-4", |
| ) |
|
|
| |
| assert response is not None |
| if "All providers failed" not in response: |
| |
| assert len(response) > 0 |
|
|
|
|
| |
| |
| |
|
|
| class TestRateLimitingHTTPLevel: |
| """ |
| Rate limiting tests using HTTP-level mocking. |
| |
| Coverage: Rate limiting logic in BYOKHandler |
| Tests: 429 responses, retry logic, exponential backoff |
| """ |
|
|
| def test_429_rate_limit_retry(self, byok_handler, mock_http_error): |
| """ |
| Test 429 rate limit response with retry. |
| |
| Coverage: Rate limit handling |
| Tests: Retry-After header, eventual success |
| """ |
| |
| error_response = mock_http_error(429, "rate_limit_error") |
|
|
| |
| assert error_response.status_code == 429 |
|
|
| error_data = error_response.json() |
| assert error_data["error"]["type"] == "rate_limit_error" |
| assert "rate limit" in error_data["error"]["message"].lower() |
|
|
| def test_rate_limit_backoff(self, byok_handler): |
| """ |
| Test exponential backoff on rate limit. |
| |
| Coverage: Retry logic with backoff |
| Tests: Multiple 429 responses, backoff calculation |
| """ |
| |
| rate_limit_count = [0] |
|
|
| def mock_check_limit(*args, **kwargs): |
| rate_limit_count[0] += 1 |
| |
| return rate_limit_count[0] > 3 |
|
|
| |
| results = [] |
| for i in range(5): |
| allowed = mock_check_limit("test_user", "test_workspace") |
| results.append(allowed) |
|
|
| |
| assert results == [False, False, False, True, True] |
|
|
| def test_concurrent_request_limiting(self, byok_handler): |
| """ |
| Test concurrent request rate limiting. |
| |
| Coverage: Concurrent request handling |
| Tests: Request queuing/throttling |
| """ |
| import asyncio |
|
|
| |
| concurrent_count = [0] |
| max_concurrent = [0] |
|
|
| async def mock_request(): |
| concurrent_count[0] += 1 |
| max_concurrent[0] = max(max_concurrent[0], concurrent_count[0]) |
| await asyncio.sleep(0.01) |
| concurrent_count[0] -= 1 |
| return True |
|
|
| async def make_requests(): |
| tasks = [mock_request() for _ in range(10)] |
| return await asyncio.gather(*tasks) |
|
|
| |
| results = asyncio.run(make_requests()) |
|
|
| |
| assert all(results) |
| assert max_concurrent[0] > 1 |
|
|
|
|
| class TestErrorHandlingHTTPLevel: |
| """ |
| Error handling tests using HTTP-level mocking. |
| |
| Coverage: Error handling paths in BYOKHandler |
| Tests: 401, 500, timeouts, malformed responses |
| """ |
|
|
| def test_401_unauthorized_recovery(self, byok_handler, mock_http_error): |
| """ |
| Test 401 unauthorized error handling. |
| |
| Coverage: Authentication error handling |
| Tests: Error parsing, API key refresh attempt |
| """ |
| error_response = mock_http_error(401, "authentication_error") |
|
|
| assert error_response.status_code == 401 |
|
|
| error_data = error_response.json() |
| assert error_data["error"]["type"] == "authentication_error" |
| assert "api key" in error_data["error"]["message"].lower() or "unauthorized" in error_data["error"]["message"].lower() |
|
|
| def test_500_internal_server_error(self, byok_handler, mock_http_error): |
| """ |
| Test 500 server error handling. |
| |
| Coverage: Server error handling |
| Tests: Retry logic, fallback provider |
| """ |
| error_response = mock_http_error(500, "server_error") |
|
|
| assert error_response.status_code == 500 |
|
|
| error_data = error_response.json() |
| assert error_data["error"]["type"] == "server_error" |
| assert "server" in error_data["error"]["message"].lower() or "internal" in error_data["error"]["message"].lower() |
|
|
| def test_network_timeout(self, byok_handler): |
| """ |
| Test network timeout handling. |
| |
| Coverage: Timeout error handling |
| Tests: Graceful degradation |
| """ |
| |
| def mock_timeout_request(*args, **kwargs): |
| raise TimeoutError("Network timeout") |
|
|
| |
| with pytest.raises(TimeoutError): |
| mock_timeout_request("https://api.openai.com/v1/chat/completions") |
|
|
| def test_malformed_response(self, byok_handler, mock_malformed_response): |
| """ |
| Test malformed JSON response handling. |
| |
| Coverage: Response parsing error handling |
| Tests: Error parsing, error logging |
| """ |
| response = mock_malformed_response( |
| "https://api.openai.com/v1/chat/completions", |
| json={"model": "gpt-4"} |
| ) |
|
|
| |
| with pytest.raises(json.JSONDecodeError): |
| response.json() |
|
|
| |
| assert response.text is not None |
|
|
| def test_empty_response(self, byok_handler): |
| """ |
| Test empty response body handling. |
| |
| Coverage: Empty response handling |
| Tests: Graceful handling of empty responses |
| """ |
| mock_response = Mock() |
| mock_response.status_code = 200 |
| mock_response.text = "" |
| mock_response.json.return_value = {} |
|
|
| |
| assert mock_response.status_code == 200 |
| assert mock_response.text == "" |
|
|
| @pytest.mark.parametrize("status_code,error_type,expected_fields", [ |
| (400, "invalid_request_error", ["type", "message"]), |
| (401, "authentication_error", ["type", "message"]), |
| (429, "rate_limit_error", ["type", "message"]), |
| (500, "server_error", ["type", "message"]), |
| (503, "server_error", ["type", "message"]), |
| ]) |
| def test_all_error_types(self, byok_handler, mock_http_error, status_code, error_type, expected_fields): |
| """ |
| Test all error response types. |
| |
| Coverage: Comprehensive error type handling |
| Tests: Error parsing for all error types |
| """ |
| error_response = mock_http_error(status_code, error_type) |
|
|
| assert error_response.status_code == status_code |
|
|
| error_data = error_response.json() |
| assert "error" in error_data |
|
|
| for field in expected_fields: |
| assert field in error_data["error"] |
|
|
| assert error_data["error"]["type"] == error_type |
|
|
|
|
|
|
| def test_provider_fallback_on_error(self, byok_handler, mock_openai_http, mock_http_error): |
| """ |
| Test provider fallback when primary provider fails. |
| |
| Coverage: BYOKHandler._get_provider_fallback_order() |
| Tests: Primary provider failure, fallback to secondary provider |
| """ |
| |
| def mock_failing_post(*args, **kwargs): |
| return mock_http_error(500, "server_error") |
|
|
| |
| def mock_success_post(*args, **kwargs): |
| return mock_deepseek_http(*args, **kwargs) |
|
|
| with patch('requests.post', side_effect=mock_failing_post): |
| |
| with pytest.raises(Exception): |
| byok_handler.generate_response( |
| prompt="Test prompt", |
| provider="openai", |
| model="gpt-4", |
| stream=False |
| ) |
|
|
| |
| fallback_order = byok_handler._get_provider_fallback_order("openai") |
| assert "openai" in fallback_order |
| assert len(fallback_order) > 1 |
|
|
| @pytest.mark.parametrize("status_code,error_type", [ |
| (400, "invalid_request_error"), |
| (401, "authentication_error"), |
| (429, "rate_limit_error"), |
| (500, "server_error"), |
| (503, "server_error"), |
| ]) |
| def test_openai_http_errors(self, byok_handler, mock_http_error, status_code, error_type): |
| """ |
| Test OpenAI HTTP error responses. |
| |
| Coverage: BYOKHandler error handling in _call_openai() |
| Tests: Error parsing, logging, graceful degradation |
| """ |
| def mock_error_post(*args, **kwargs): |
| return mock_http_error(status_code, error_type) |
|
|
| with patch('requests.post', mock_error_post): |
| |
| with pytest.raises(Exception): |
| byok_handler.generate_response( |
| prompt="Test prompt", |
| provider="openai", |
| model="gpt-4", |
| stream=False |
| ) |
|
|
| @pytest.mark.parametrize("status_code,error_type", [ |
| (400, "invalid_request_error"), |
| (401, "authentication_error"), |
| (429, "rate_limit_error"), |
| (500, "server_error"), |
| ]) |
| def test_anthropic_http_errors(self, byok_handler, mock_http_error, status_code, error_type): |
| """ |
| Test Anthropic HTTP error responses. |
| |
| Coverage: BYOKHandler error handling in _call_anthropic() |
| Tests: Error parsing, Anthropic-specific error codes |
| """ |
| def mock_error_post(*args, **kwargs): |
| return mock_http_error(status_code, error_type) |
|
|
| with patch('requests.post', mock_error_post): |
| with pytest.raises(Exception): |
| byok_handler.generate_response( |
| prompt="Test prompt", |
| provider="anthropic", |
| model="claude-3-opus-20240229", |
| stream=False |
| ) |
|
|
| def test_request_headers_verification(self, byok_handler, mock_openai_http): |
| """ |
| Test HTTP request includes correct headers. |
| |
| Coverage: Request formatting in _call_* methods |
| Tests: Authorization, content-type, provider-specific headers |
| """ |
| captured_headers = {} |
|
|
| def mock_capture_headers(*args, **kwargs): |
| captured_headers.update(kwargs.get('headers', {})) |
| return mock_openai_http(*args, **kwargs) |
|
|
| with patch('requests.post', mock_capture_headers): |
| try: |
| byok_handler.generate_response( |
| prompt="Test", |
| provider="openai", |
| model="gpt-4", |
| stream=False |
| ) |
| except: |
| pass |
|
|
| |
| |
|
|
| def test_request_body_verification(self, byok_handler, mock_openai_http): |
| """ |
| Test HTTP request includes correct body structure. |
| |
| Coverage: Request formatting in _call_* methods |
| Tests: messages array, model field, temperature, max_tokens |
| """ |
| captured_body = {} |
|
|
| def mock_capture_body(*args, **kwargs): |
| captured_body.update(kwargs.get('json', {})) |
| return mock_openai_http(*args, **kwargs) |
|
|
| with patch('requests.post', mock_capture_body): |
| try: |
| byok_handler.generate_response( |
| prompt="Test prompt", |
| provider="openai", |
| model="gpt-4", |
| stream=False |
| ) |
| except: |
| pass |
|
|
| |
| |
|
|