SummarizerApp / tests /test_502_prevention.py
ming
Add comprehensive unit tests for all production-ready features
0497d92
raw
history blame
8.82 kB
"""
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."""
@pytest.mark.integration
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()
@pytest.mark.integration
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
@pytest.mark.integration
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
@pytest.mark.integration
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
@pytest.mark.integration
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
@pytest.mark.integration
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()
@pytest.mark.integration
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"]
@pytest.mark.integration
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"]
@pytest.mark.integration
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
@pytest.mark.integration
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}"