Spaces:
Running
Running
| """ | |
| Tests specifically for 502 Bad Gateway error prevention. | |
| """ | |
| import pytest | |
| import httpx | |
| from unittest.mock import patch, MagicMock | |
| from starlette.testclient import TestClient | |
| from app.main import app | |
| from tests.test_services import StubAsyncClient, StubAsyncResponse | |
| client = TestClient(app) | |
| class Test502BadGatewayPrevention: | |
| """Test that 502 Bad Gateway errors are prevented and handled properly.""" | |
| def test_no_502_for_timeout_errors(self): | |
| """Test that timeout errors return 504 instead of 502.""" | |
| with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=httpx.TimeoutException("Timeout"))): | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": "Test text that will timeout"} | |
| ) | |
| # Should return 504 Gateway Timeout, not 502 Bad Gateway | |
| assert resp.status_code == 504 | |
| assert resp.status_code != 502 | |
| data = resp.json() | |
| assert "timeout" in data["detail"].lower() | |
| assert "text may be too long" in data["detail"].lower() | |
| def test_large_text_gets_extended_timeout(self): | |
| """Test that large text gets extended timeout to prevent 502 errors.""" | |
| large_text = "A" * 10000 # 10,000 characters | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse()) | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": large_text, "max_tokens": 256} | |
| ) | |
| # Verify extended timeout was used | |
| mock_client.assert_called_once() | |
| call_args = mock_client.call_args | |
| expected_timeout = 120 + (10000 - 1000) // 1000 * 10 # 210 seconds | |
| assert call_args[1]['timeout'] == expected_timeout | |
| def test_very_large_text_gets_capped_timeout(self): | |
| """Test that very large text gets capped timeout to prevent infinite waits.""" | |
| very_large_text = "A" * 100000 # 100,000 characters | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse()) | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": very_large_text, "max_tokens": 256} | |
| ) | |
| # Verify timeout is capped at 300 seconds | |
| mock_client.assert_called_once() | |
| call_args = mock_client.call_args | |
| assert call_args[1]['timeout'] == 300 # Maximum cap | |
| def test_small_text_uses_base_timeout(self): | |
| """Test that small text uses base timeout (30 seconds).""" | |
| small_text = "Short text" | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse()) | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": small_text, "max_tokens": 256} | |
| ) | |
| # Verify base timeout was used | |
| mock_client.assert_called_once() | |
| call_args = mock_client.call_args | |
| assert call_args[1]['timeout'] == 120 # Base timeout | |
| def test_medium_text_gets_appropriate_timeout(self): | |
| """Test that medium-sized text gets appropriate timeout.""" | |
| medium_text = "A" * 5000 # 5,000 characters | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse()) | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": medium_text, "max_tokens": 256} | |
| ) | |
| # Verify appropriate timeout was used | |
| mock_client.assert_called_once() | |
| call_args = mock_client.call_args | |
| expected_timeout = 120 + (5000 - 1000) // 1000 * 10 # 160 seconds | |
| assert call_args[1]['timeout'] == expected_timeout | |
| def test_timeout_error_has_helpful_message(self): | |
| """Test that timeout errors provide helpful guidance.""" | |
| with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=httpx.TimeoutException("Timeout"))): | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": "Test text"} | |
| ) | |
| assert resp.status_code == 504 | |
| data = resp.json() | |
| # Check for helpful error message | |
| assert "timeout" in data["detail"].lower() | |
| assert "text may be too long" in data["detail"].lower() | |
| assert "reduce" in data["detail"].lower() | |
| assert "max_tokens" in data["detail"].lower() | |
| def test_http_errors_still_return_502(self): | |
| """Test that actual HTTP errors still return 502 (this is correct behavior).""" | |
| http_error = httpx.HTTPStatusError("Bad Request", request=MagicMock(), response=MagicMock()) | |
| with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=http_error)): | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": "Test text"} | |
| ) | |
| # HTTP errors should still return 502 | |
| assert resp.status_code == 502 | |
| data = resp.json() | |
| assert "Summarization failed" in data["detail"] | |
| def test_unexpected_errors_return_500(self): | |
| """Test that unexpected errors return 500 Internal Server Error.""" | |
| with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_exc=Exception("Unexpected error"))): | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": "Test text"} | |
| ) | |
| assert resp.status_code == 500 | |
| data = resp.json() | |
| assert "Internal server error" in data["detail"] | |
| def test_successful_large_text_processing(self): | |
| """Test that large text can be processed successfully with extended timeout.""" | |
| large_text = "A" * 5000 # 5,000 characters | |
| mock_response = { | |
| "response": "This is a summary of the large text.", | |
| "eval_count": 25, | |
| "done": True | |
| } | |
| with patch('httpx.AsyncClient', return_value=StubAsyncClient(post_result=StubAsyncResponse(json_data=mock_response))): | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": large_text, "max_tokens": 256} | |
| ) | |
| # Should succeed with 200 | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert data["summary"] == mock_response["response"] | |
| assert data["model"] == "llama3.2:latest" | |
| assert data["tokens_used"] == mock_response["eval_count"] | |
| assert "latency_ms" in data | |
| def test_dynamic_timeout_calculation_formula(self): | |
| """Test the exact formula for dynamic timeout calculation.""" | |
| test_cases = [ | |
| (500, 120), # Small text: base timeout (120s) | |
| (1000, 120), # Exactly 1000 chars: base timeout (120s) | |
| (1500, 120), # 1500 chars: 120 + (500//1000)*10 = 120 + 0*10 = 120 | |
| (2000, 130), # 2000 chars: 120 + (1000//1000)*10 = 120 + 1*10 = 130 | |
| (5000, 160), # 5000 chars: 120 + (4000//1000)*10 = 120 + 4*10 = 160 | |
| (10000, 210), # 10000 chars: 120 + (9000//1000)*10 = 120 + 9*10 = 210 | |
| (50000, 300), # Very large: should be capped at 300 | |
| ] | |
| for text_length, expected_timeout in test_cases: | |
| test_text = "A" * text_length | |
| with patch('httpx.AsyncClient') as mock_client: | |
| mock_client.return_value = StubAsyncClient(post_result=StubAsyncResponse()) | |
| resp = client.post( | |
| "/api/v1/summarize/", | |
| json={"text": test_text, "max_tokens": 256} | |
| ) | |
| # Verify timeout calculation | |
| mock_client.assert_called_once() | |
| call_args = mock_client.call_args | |
| actual_timeout = call_args[1]['timeout'] | |
| assert actual_timeout == expected_timeout, f"Text length {text_length} should have timeout {expected_timeout}, got {actual_timeout}" | |