anderson-ufrj commited on
Commit
66f9c89
·
1 Parent(s): daf23ba

test(chat): add comprehensive tests for emergency chat endpoint

Browse files

- Add unit tests for intent detection and Maritaca integration
- Add integration tests for performance and concurrency
- Test fallback behavior when external services fail
- Verify session continuity and error recovery
- Test response variety and metadata consistency
- Add health endpoint monitoring tests

tests/integration/test_chat_emergency_integration.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Integration tests for emergency chat endpoint"""
2
+
3
+ import pytest
4
+ import asyncio
5
+ from fastapi.testclient import TestClient
6
+ from unittest.mock import patch
7
+ import time
8
+
9
+ from src.api.routes.chat_emergency import router
10
+
11
+
12
+ class TestEmergencyEndpointIntegration:
13
+ """Integration tests for emergency chat endpoint"""
14
+
15
+ @pytest.fixture
16
+ def app(self):
17
+ """Create FastAPI app with emergency router"""
18
+ from fastapi import FastAPI
19
+ app = FastAPI()
20
+ app.include_router(router)
21
+ return app
22
+
23
+ @pytest.fixture
24
+ def client(self, app):
25
+ """Create test client"""
26
+ return TestClient(app)
27
+
28
+ def test_emergency_endpoint_performance(self, client):
29
+ """Test emergency endpoint response time"""
30
+ messages = [
31
+ "Olá!",
32
+ "Quero investigar contratos",
33
+ "Como funciona o sistema?",
34
+ "Mostre gastos de 2024",
35
+ "Analise fornecedor XYZ"
36
+ ]
37
+
38
+ response_times = []
39
+
40
+ for message in messages:
41
+ start_time = time.time()
42
+ response = client.post(
43
+ "/api/v1/chat/emergency",
44
+ json={"message": message}
45
+ )
46
+ end_time = time.time()
47
+
48
+ assert response.status_code == 200
49
+ response_times.append(end_time - start_time)
50
+
51
+ # Verify all responses are fast (under 100ms for fallback)
52
+ assert all(t < 0.1 for t in response_times), f"Response times: {response_times}"
53
+
54
+ # Average should be very fast
55
+ avg_time = sum(response_times) / len(response_times)
56
+ assert avg_time < 0.05, f"Average response time: {avg_time}"
57
+
58
+ def test_emergency_endpoint_concurrent_requests(self, client):
59
+ """Test emergency endpoint under concurrent load"""
60
+ import concurrent.futures
61
+
62
+ def make_request(message):
63
+ response = client.post(
64
+ "/api/v1/chat/emergency",
65
+ json={"message": message}
66
+ )
67
+ return response.status_code, response.json()
68
+
69
+ messages = [f"Teste concorrente {i}" for i in range(20)]
70
+
71
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
72
+ futures = [executor.submit(make_request, msg) for msg in messages]
73
+ results = [future.result() for future in concurrent.futures.as_completed(futures)]
74
+
75
+ # All requests should succeed
76
+ assert all(status == 200 for status, _ in results)
77
+
78
+ # All should have valid responses
79
+ for _, data in results:
80
+ assert "session_id" in data
81
+ assert "message" in data
82
+ assert len(data["message"]) > 0
83
+
84
+ def test_emergency_endpoint_session_continuity(self, client):
85
+ """Test session continuity across multiple messages"""
86
+ session_id = "test-session-continuity"
87
+
88
+ messages = [
89
+ "Olá!",
90
+ "Quero investigar contratos",
91
+ "Mostre os mais recentes"
92
+ ]
93
+
94
+ for i, message in enumerate(messages):
95
+ response = client.post(
96
+ "/api/v1/chat/emergency",
97
+ json={"message": message, "session_id": session_id}
98
+ )
99
+
100
+ assert response.status_code == 200
101
+ data = response.json()
102
+ assert data["session_id"] == session_id
103
+
104
+ # Verify response makes sense for the intent
105
+ if i == 0: # Greeting
106
+ assert data["metadata"]["intent"] == "greeting"
107
+ elif i == 1: # Investigation
108
+ assert data["metadata"]["intent"] == "investigation"
109
+
110
+ def test_emergency_endpoint_error_recovery(self, client):
111
+ """Test emergency endpoint handles errors gracefully"""
112
+ # Test with various edge cases
113
+ edge_cases = [
114
+ {"message": "a"}, # Very short
115
+ {"message": "?" * 100}, # Special characters
116
+ {"message": "SELECT * FROM users"}, # SQL-like
117
+ {"message": "<script>alert('test')</script>"}, # XSS attempt
118
+ {"message": "😀" * 50}, # Emojis
119
+ ]
120
+
121
+ for payload in edge_cases:
122
+ response = client.post(
123
+ "/api/v1/chat/emergency",
124
+ json=payload
125
+ )
126
+
127
+ assert response.status_code == 200
128
+ data = response.json()
129
+ assert "message" in data
130
+ assert len(data["message"]) > 0
131
+ assert "error" not in data
132
+
133
+ @pytest.mark.asyncio
134
+ async def test_emergency_endpoint_maritaca_fallback(self, client):
135
+ """Test fallback when Maritaca fails"""
136
+ with patch.dict('os.environ', {'MARITACA_API_KEY': 'test-key'}):
137
+ # Mock Maritaca to fail
138
+ with patch('httpx.AsyncClient') as mock_client:
139
+ mock_instance = mock_client.return_value.__aenter__.return_value
140
+ mock_instance.post.side_effect = Exception("API Error")
141
+
142
+ response = client.post(
143
+ "/api/v1/chat/emergency",
144
+ json={"message": "Test with failed Maritaca"}
145
+ )
146
+
147
+ assert response.status_code == 200
148
+ data = response.json()
149
+
150
+ # Should fallback to intelligent responses
151
+ assert data["agent_id"] == "system"
152
+ assert data["metadata"]["backend"] == "intelligent_fallback"
153
+ assert len(data["message"]) > 0
154
+
155
+ def test_emergency_endpoint_response_variety(self, client):
156
+ """Test that responses vary for same intent"""
157
+ responses = []
158
+
159
+ # Send same greeting multiple times
160
+ for _ in range(5):
161
+ response = client.post(
162
+ "/api/v1/chat/emergency",
163
+ json={"message": "Olá!"}
164
+ )
165
+
166
+ assert response.status_code == 200
167
+ data = response.json()
168
+ responses.append(data["message"])
169
+
170
+ # Should have some variety in responses
171
+ unique_responses = set(responses)
172
+ assert len(unique_responses) >= 2, "Responses should vary"
173
+
174
+ def test_emergency_health_monitoring(self, client):
175
+ """Test health endpoint provides accurate status"""
176
+ # Test without Maritaca
177
+ with patch.dict('os.environ', {}, clear=True):
178
+ response = client.get("/api/v1/chat/emergency/health")
179
+ assert response.status_code == 200
180
+ data = response.json()
181
+ assert data["status"] == "operational"
182
+ assert data["maritaca_configured"] is False
183
+
184
+ # Test with Maritaca
185
+ with patch.dict('os.environ', {'MARITACA_API_KEY': 'test-key'}):
186
+ response = client.get("/api/v1/chat/emergency/health")
187
+ assert response.status_code == 200
188
+ data = response.json()
189
+ assert data["maritaca_configured"] is True
190
+
191
+ def test_emergency_endpoint_metadata_consistency(self, client):
192
+ """Test metadata is consistent and complete"""
193
+ response = client.post(
194
+ "/api/v1/chat/emergency",
195
+ json={"message": "Test metadata"}
196
+ )
197
+
198
+ assert response.status_code == 200
199
+ data = response.json()
200
+
201
+ # Check required metadata fields
202
+ assert "backend" in data["metadata"]
203
+ assert "timestamp" in data["metadata"]
204
+ assert "intent" in data["metadata"]
205
+
206
+ # Timestamp should be valid ISO format
207
+ from datetime import datetime
208
+ timestamp = datetime.fromisoformat(data["metadata"]["timestamp"])
209
+ assert timestamp.year >= 2024
tests/unit/test_chat_emergency.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for emergency chat endpoint"""
2
+
3
+ import pytest
4
+ from unittest.mock import patch, AsyncMock
5
+ from fastapi.testclient import TestClient
6
+ from httpx import Response
7
+ import json
8
+
9
+ from src.api.routes.chat_emergency import (
10
+ router,
11
+ detect_intent,
12
+ try_maritaca,
13
+ ChatRequest,
14
+ ChatResponse
15
+ )
16
+
17
+
18
+ class TestDetectIntent:
19
+ """Test intent detection function"""
20
+
21
+ def test_detect_greeting_intent(self):
22
+ """Test greeting intent detection"""
23
+ greetings = ["olá", "Oi", "bom dia", "Boa tarde", "BOA NOITE", "prazer"]
24
+ for greeting in greetings:
25
+ assert detect_intent(greeting) == "greeting"
26
+ assert detect_intent(f"{greeting}, como vai?") == "greeting"
27
+
28
+ def test_detect_investigation_intent(self):
29
+ """Test investigation intent detection"""
30
+ investigations = [
31
+ "investigar contratos",
32
+ "verificar gastos",
33
+ "analisar fornecedores",
34
+ "buscar licitações",
35
+ "procurar irregularidades"
36
+ ]
37
+ for text in investigations:
38
+ assert detect_intent(text) == "investigation"
39
+
40
+ def test_detect_help_intent(self):
41
+ """Test help intent detection"""
42
+ help_texts = [
43
+ "ajuda",
44
+ "pode me ajudar",
45
+ "o que você consegue fazer",
46
+ "como funciona",
47
+ "o que posso perguntar"
48
+ ]
49
+ for text in help_texts:
50
+ assert detect_intent(text) == "help"
51
+
52
+ def test_detect_default_intent(self):
53
+ """Test default intent for unmatched texts"""
54
+ defaults = [
55
+ "quero saber sobre contratos",
56
+ "mostre gastos de 2024",
57
+ "fornecedor XYZ"
58
+ ]
59
+ for text in defaults:
60
+ assert detect_intent(text) == "default"
61
+
62
+
63
+ class TestTryMaritaca:
64
+ """Test Maritaca AI integration"""
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_maritaca_without_api_key(self):
68
+ """Test Maritaca returns None without API key"""
69
+ with patch.dict('os.environ', {}, clear=True):
70
+ result = await try_maritaca("test message")
71
+ assert result is None
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_maritaca_successful_request(self):
75
+ """Test successful Maritaca API call"""
76
+ with patch.dict('os.environ', {'MARITACA_API_KEY': 'test-key'}):
77
+ mock_response = AsyncMock()
78
+ mock_response.status_code = 200
79
+ mock_response.json.return_value = {"answer": "Test response from Maritaca"}
80
+
81
+ with patch('httpx.AsyncClient') as mock_client:
82
+ mock_instance = mock_client.return_value.__aenter__.return_value
83
+ mock_instance.post.return_value = mock_response
84
+
85
+ result = await try_maritaca("test message")
86
+ assert result == "Test response from Maritaca"
87
+
88
+ # Verify API call was made correctly
89
+ mock_instance.post.assert_called_once()
90
+ call_args = mock_instance.post.call_args
91
+ assert call_args[0][0] == "https://chat.maritaca.ai/api/chat/inference"
92
+ assert "authorization" in call_args[1]["headers"]
93
+ assert "Bearer test-key" in call_args[1]["headers"]["authorization"]
94
+
95
+ @pytest.mark.asyncio
96
+ async def test_maritaca_api_error(self):
97
+ """Test Maritaca API error handling"""
98
+ with patch.dict('os.environ', {'MARITACA_API_KEY': 'test-key'}):
99
+ mock_response = AsyncMock()
100
+ mock_response.status_code = 500
101
+
102
+ with patch('httpx.AsyncClient') as mock_client:
103
+ mock_instance = mock_client.return_value.__aenter__.return_value
104
+ mock_instance.post.return_value = mock_response
105
+
106
+ result = await try_maritaca("test message")
107
+ assert result is None
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_maritaca_network_error(self):
111
+ """Test Maritaca network error handling"""
112
+ with patch.dict('os.environ', {'MARITACA_API_KEY': 'test-key'}):
113
+ with patch('httpx.AsyncClient') as mock_client:
114
+ mock_instance = mock_client.return_value.__aenter__.return_value
115
+ mock_instance.post.side_effect = Exception("Network error")
116
+
117
+ result = await try_maritaca("test message")
118
+ assert result is None
119
+
120
+
121
+ class TestChatEmergencyEndpoint:
122
+ """Test emergency chat endpoint"""
123
+
124
+ @pytest.fixture
125
+ def client(self):
126
+ """Create test client"""
127
+ from fastapi import FastAPI
128
+ app = FastAPI()
129
+ app.include_router(router)
130
+ return TestClient(app)
131
+
132
+ def test_emergency_endpoint_greeting(self, client):
133
+ """Test emergency endpoint with greeting"""
134
+ response = client.post(
135
+ "/api/v1/chat/emergency",
136
+ json={"message": "Olá, bom dia!"}
137
+ )
138
+
139
+ assert response.status_code == 200
140
+ data = response.json()
141
+
142
+ assert "session_id" in data
143
+ assert data["agent_id"] == "system"
144
+ assert data["agent_name"] == "Assistente Cidadão.AI"
145
+ assert len(data["message"]) > 0
146
+ assert data["confidence"] == 0.85
147
+ assert len(data["suggested_actions"]) > 0
148
+ assert data["metadata"]["intent"] == "greeting"
149
+ assert data["metadata"]["backend"] == "intelligent_fallback"
150
+
151
+ def test_emergency_endpoint_investigation(self, client):
152
+ """Test emergency endpoint with investigation request"""
153
+ response = client.post(
154
+ "/api/v1/chat/emergency",
155
+ json={"message": "Quero investigar contratos suspeitos"}
156
+ )
157
+
158
+ assert response.status_code == 200
159
+ data = response.json()
160
+
161
+ assert data["metadata"]["intent"] == "investigation"
162
+ assert "search_contracts" in data["suggested_actions"]
163
+
164
+ def test_emergency_endpoint_help(self, client):
165
+ """Test emergency endpoint with help request"""
166
+ response = client.post(
167
+ "/api/v1/chat/emergency",
168
+ json={"message": "Como posso usar o sistema?"}
169
+ )
170
+
171
+ assert response.status_code == 200
172
+ data = response.json()
173
+
174
+ assert data["metadata"]["intent"] == "help"
175
+ assert any(action in ["help", "examples", "documentation"] for action in data["suggested_actions"])
176
+
177
+ def test_emergency_endpoint_with_session_id(self, client):
178
+ """Test emergency endpoint preserves session_id"""
179
+ session_id = "test-session-123"
180
+ response = client.post(
181
+ "/api/v1/chat/emergency",
182
+ json={"message": "teste", "session_id": session_id}
183
+ )
184
+
185
+ assert response.status_code == 200
186
+ data = response.json()
187
+ assert data["session_id"] == session_id
188
+
189
+ def test_emergency_endpoint_without_session_id(self, client):
190
+ """Test emergency endpoint generates session_id"""
191
+ response = client.post(
192
+ "/api/v1/chat/emergency",
193
+ json={"message": "teste"}
194
+ )
195
+
196
+ assert response.status_code == 200
197
+ data = response.json()
198
+ assert "session_id" in data
199
+ assert data["session_id"].startswith("emergency_")
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_emergency_endpoint_with_maritaca(self, client):
203
+ """Test emergency endpoint using Maritaca"""
204
+ with patch.dict('os.environ', {'MARITACA_API_KEY': 'test-key'}):
205
+ with patch('src.api.routes.chat_emergency.try_maritaca') as mock_maritaca:
206
+ mock_maritaca.return_value = "Response from Maritaca AI"
207
+
208
+ response = client.post(
209
+ "/api/v1/chat/emergency",
210
+ json={"message": "teste com maritaca"}
211
+ )
212
+
213
+ assert response.status_code == 200
214
+ data = response.json()
215
+
216
+ assert data["agent_id"] == "maritaca"
217
+ assert data["agent_name"] == "Assistente Cidadão.AI (Maritaca)"
218
+ assert data["message"] == "Response from Maritaca AI"
219
+ assert data["confidence"] == 0.95
220
+ assert data["metadata"]["backend"] == "maritaca_ai"
221
+ assert data["metadata"]["model"] == "sabia-3"
222
+
223
+ def test_emergency_endpoint_validation_error(self, client):
224
+ """Test emergency endpoint with invalid input"""
225
+ # Empty message
226
+ response = client.post(
227
+ "/api/v1/chat/emergency",
228
+ json={"message": ""}
229
+ )
230
+ assert response.status_code == 422
231
+
232
+ # Message too long
233
+ response = client.post(
234
+ "/api/v1/chat/emergency",
235
+ json={"message": "a" * 1001}
236
+ )
237
+ assert response.status_code == 422
238
+
239
+ # Missing message
240
+ response = client.post(
241
+ "/api/v1/chat/emergency",
242
+ json={}
243
+ )
244
+ assert response.status_code == 422
245
+
246
+
247
+ class TestEmergencyHealthEndpoint:
248
+ """Test emergency health check endpoint"""
249
+
250
+ @pytest.fixture
251
+ def client(self):
252
+ """Create test client"""
253
+ from fastapi import FastAPI
254
+ app = FastAPI()
255
+ app.include_router(router)
256
+ return TestClient(app)
257
+
258
+ def test_health_endpoint_without_maritaca(self, client):
259
+ """Test health endpoint without Maritaca API key"""
260
+ with patch.dict('os.environ', {}, clear=True):
261
+ response = client.get("/api/v1/chat/emergency/health")
262
+
263
+ assert response.status_code == 200
264
+ data = response.json()
265
+
266
+ assert data["status"] == "operational"
267
+ assert data["endpoint"] == "/api/v1/chat/emergency"
268
+ assert data["maritaca_configured"] is False
269
+ assert data["fallback_ready"] is True
270
+ assert "timestamp" in data
271
+
272
+ def test_health_endpoint_with_maritaca(self, client):
273
+ """Test health endpoint with Maritaca API key"""
274
+ with patch.dict('os.environ', {'MARITACA_API_KEY': 'test-key'}):
275
+ response = client.get("/api/v1/chat/emergency/health")
276
+
277
+ assert response.status_code == 200
278
+ data = response.json()
279
+
280
+ assert data["maritaca_configured"] is True