anderson-ufrj commited on
Commit
0d31b85
·
1 Parent(s): 60716dd

test(api): update API endpoint integration tests

Browse files

- Add comprehensive auth endpoint tests
- Add investigation endpoint tests
- Add analysis endpoint tests
- Add health and metrics tests
- Add report generation tests
- Test audit trail endpoints

Files changed (1) hide show
  1. tests/integration/test_api_endpoints.py +402 -490
tests/integration/test_api_endpoints.py CHANGED
@@ -1,575 +1,487 @@
1
- """
2
- Module: tests.integration.test_api_endpoints
3
- Description: Comprehensive integration tests for FastAPI endpoints
4
- Author: Anderson H. Silva
5
- Date: 2025-01-24
6
- License: Proprietary - All rights reserved
7
- """
8
-
9
  import pytest
10
- import asyncio
11
- from datetime import datetime
12
- from typing import Dict, Any
13
- from unittest.mock import AsyncMock, patch
14
-
15
  from fastapi.testclient import TestClient
16
- from httpx import AsyncClient
17
-
18
- from src.api.app import app
19
- from src.core import AgentStatus, ResponseStatus
20
-
21
-
22
- # Test client for synchronous tests
23
- client = TestClient(app)
24
-
25
 
26
- @pytest.fixture
27
- async def async_client():
28
- """Async test client for async endpoints."""
29
- async with AsyncClient(app=app, base_url="http://test") as ac:
30
- yield ac
31
 
32
 
33
- @pytest.fixture
34
- def mock_transparency_api():
35
- """Mock the transparency API client."""
36
- with patch('src.tools.transparency_api.TransparencyAPIClient') as mock:
37
- mock_instance = AsyncMock()
38
- mock.__aenter__.return_value = mock_instance
39
- mock_instance.get_contracts.return_value = AsyncMock(
40
- data=[
41
- {
42
- "id": "123",
43
- "objeto": "Test contract",
44
- "valorInicial": 100000.0,
45
- "fornecedor": {"nome": "Test Supplier"}
46
- }
47
- ]
 
 
 
48
  )
49
- yield mock_instance
50
-
51
-
52
- @pytest.fixture
53
- def mock_agents():
54
- """Mock all agents for testing."""
55
- with patch('src.agents.master_agent.MasterAgent') as mock_master, \
56
- patch('src.agents.investigator_agent.InvestigatorAgent') as mock_investigator, \
57
- patch('src.agents.analyst_agent.AnalystAgent') as mock_analyst, \
58
- patch('src.agents.reporter_agent.ReporterAgent') as mock_reporter:
59
-
60
- # Configure mock responses
61
- mock_master.return_value.investigate.return_value = {
62
- "status": "completed",
63
- "findings": ["Test finding"],
64
- "anomalies": []
65
- }
66
-
67
- mock_investigator.return_value.detect_anomalies.return_value = {
68
- "anomalies": [],
69
- "score": 0.1,
70
- "explanation": "No anomalies detected"
71
- }
72
-
73
- mock_analyst.return_value.analyze_patterns.return_value = {
74
- "patterns": [],
75
- "trends": [],
76
- "correlations": []
77
- }
78
-
79
- mock_reporter.return_value.generate_report.return_value = {
80
- "content": "Test report content",
81
- "format": "markdown"
82
- }
83
-
84
- yield {
85
- "master": mock_master,
86
- "investigator": mock_investigator,
87
- "analyst": mock_analyst,
88
- "reporter": mock_reporter
89
- }
90
-
91
-
92
- class TestHealthEndpoints:
93
- """Test health check endpoints."""
94
 
95
- def test_basic_health_check(self):
96
- """Test basic health endpoint."""
97
- response = client.get("/health/")
 
 
 
 
 
 
 
 
98
 
99
- assert response.status_code == 200
100
  data = response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- assert data["status"] in ["healthy", "degraded", "unhealthy"]
103
- assert "timestamp" in data
104
- assert "version" in data
105
- assert "uptime" in data
106
- assert "services" in data
107
 
108
- def test_detailed_health_check(self):
109
- """Test detailed health endpoint."""
110
- response = client.get("/health/detailed")
 
 
 
 
 
 
 
 
111
 
112
  assert response.status_code == 200
113
  data = response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
- assert "system" in data
116
- assert "services" in data
117
- assert "performance" in data
118
 
119
- def test_liveness_probe(self):
120
- """Test Kubernetes liveness probe."""
121
- response = client.get("/health/live")
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  assert response.status_code == 200
124
  data = response.json()
125
- assert data["status"] == "alive"
 
126
 
127
- def test_readiness_probe(self):
128
- """Test Kubernetes readiness probe."""
129
- response = client.get("/health/ready")
 
 
 
130
 
131
- # Could be 200 or 503 depending on dependencies
132
- assert response.status_code in [200, 503]
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- data = response.json()
135
- assert "status" in data
136
 
137
 
138
  class TestInvestigationEndpoints:
139
  """Test investigation endpoints."""
140
 
141
- @pytest.mark.asyncio
142
- async def test_start_investigation(self, async_client, mock_agents):
143
- """Test starting a new investigation."""
144
- investigation_data = {
145
- "query": "Investigate contracts over 1M in Ministry of Health",
146
- "priority": "high",
147
- "data_sources": ["portal_transparencia"],
148
- "parameters": {
149
- "min_value": 1000000,
150
- "organization": "26000"
151
- }
152
- }
153
-
154
- response = await async_client.post(
155
- "/api/v1/investigations/start",
156
- json=investigation_data
 
157
  )
158
-
159
- assert response.status_code == 202 # Accepted for async processing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  data = response.json()
161
-
162
- assert data["status"] == "accepted"
163
- assert "investigation_id" in data
164
- assert "message" in data
165
-
166
- @pytest.mark.asyncio
167
- async def test_get_investigation_status(self, async_client):
168
- """Test getting investigation status."""
169
- investigation_id = "test-investigation-123"
170
-
171
- response = await async_client.get(
172
- f"/api/v1/investigations/{investigation_id}/status"
173
- )
174
-
175
- # Should handle non-existent investigations gracefully
176
- assert response.status_code in [200, 404]
177
 
178
- @pytest.mark.asyncio
179
- async def test_list_investigations(self, async_client):
180
  """Test listing investigations."""
181
- response = await async_client.get("/api/v1/investigations/")
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  assert response.status_code == 200
184
  data = response.json()
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- assert "investigations" in data
187
- assert "total" in data
188
- assert "page" in data
189
- assert "per_page" in data
190
-
191
- @pytest.mark.asyncio
192
- async def test_investigation_streaming(self, async_client):
193
- """Test real-time investigation streaming."""
194
- investigation_id = "test-investigation-123"
195
-
196
- # Test SSE endpoint exists
197
- response = await async_client.get(
198
- f"/api/v1/investigations/{investigation_id}/stream",
199
- headers={"Accept": "text/event-stream"}
200
- )
201
-
202
- # Should handle streaming endpoint
203
- assert response.status_code in [200, 404, 501] # 501 if not implemented yet
204
-
205
- def test_investigation_validation(self):
206
- """Test investigation request validation."""
207
- # Test empty request
208
- response = client.post("/api/v1/investigations/start", json={})
209
- assert response.status_code == 422 # Validation error
210
 
211
- # Test invalid priority
212
- invalid_data = {
213
- "query": "Test query",
214
- "priority": "invalid_priority"
215
- }
216
- response = client.post("/api/v1/investigations/start", json=invalid_data)
217
- assert response.status_code == 422
 
 
 
 
 
 
 
 
 
218
 
219
 
220
  class TestAnalysisEndpoints:
221
  """Test analysis endpoints."""
222
 
223
- @pytest.mark.asyncio
224
- async def test_spending_trends_analysis(self, async_client, mock_agents):
225
- """Test spending trends analysis."""
226
- analysis_data = {
227
- "type": "spending_trends",
228
- "organization": "26000",
229
- "time_period": {
230
- "start_date": "2024-01-01",
231
- "end_date": "2024-12-31"
232
- }
233
- }
234
-
235
- response = await async_client.post(
236
- "/api/v1/analysis/trends",
237
- json=analysis_data
238
- )
239
-
240
- assert response.status_code in [200, 202]
241
-
242
- if response.status_code == 200:
243
- data = response.json()
244
- assert "trends" in data
245
- assert "analysis_type" in data
246
-
247
- @pytest.mark.asyncio
248
- async def test_vendor_analysis(self, async_client, mock_agents):
249
- """Test vendor pattern analysis."""
250
- analysis_data = {
251
- "vendor_id": "12345678000100",
252
- "analysis_type": "pattern_detection"
253
- }
254
-
255
- response = await async_client.post(
256
- "/api/v1/analysis/vendors",
257
- json=analysis_data
258
- )
259
-
260
- assert response.status_code in [200, 202]
261
-
262
- @pytest.mark.asyncio
263
- async def test_correlation_analysis(self, async_client, mock_agents):
264
- """Test correlation analysis."""
265
- analysis_data = {
266
- "variables": ["spending_amount", "contract_duration"],
267
- "organization": "26000",
268
- "timeframe": "2024"
269
- }
270
-
271
- response = await async_client.post(
272
- "/api/v1/analysis/correlations",
273
- json=analysis_data
274
- )
275
-
276
- assert response.status_code in [200, 202]
277
-
278
-
279
- class TestReportEndpoints:
280
- """Test report generation endpoints."""
281
 
282
- @pytest.mark.asyncio
283
- async def test_generate_executive_report(self, async_client, mock_agents):
284
- """Test executive report generation."""
285
- report_data = {
286
- "type": "executive",
287
- "investigation_id": "test-investigation-123",
288
- "format": "markdown",
289
- "include_charts": True
290
- }
291
-
292
- response = await async_client.post(
293
- "/api/v1/reports/generate",
294
- json=report_data
295
- )
 
 
 
 
 
296
 
297
- assert response.status_code in [200, 202]
 
 
 
298
 
299
- @pytest.mark.asyncio
300
- async def test_get_report_formats(self, async_client):
301
- """Test getting available report formats."""
302
- response = await async_client.get("/api/v1/reports/formats")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  assert response.status_code == 200
305
  data = response.json()
306
-
307
- assert "formats" in data
308
- assert isinstance(data["formats"], list)
309
- assert "markdown" in data["formats"]
310
-
311
- @pytest.mark.asyncio
312
- async def test_download_report(self, async_client):
313
- """Test report download."""
314
- report_id = "test-report-123"
315
-
316
- response = await async_client.get(
317
- f"/api/v1/reports/{report_id}/download"
318
- )
319
-
320
- # Should handle non-existent reports
321
- assert response.status_code in [200, 404]
322
 
323
- @pytest.mark.asyncio
324
- async def test_report_list(self, async_client):
325
- """Test listing generated reports."""
326
- response = await async_client.get("/api/v1/reports/")
 
 
 
 
 
 
 
327
 
328
  assert response.status_code == 200
329
  data = response.json()
330
-
331
- assert "reports" in data
332
- assert "total" in data
333
 
334
 
335
- class TestAuthenticationEndpoints:
336
- """Test authentication endpoints (if implemented)."""
337
-
338
- def test_login_endpoint_exists(self):
339
- """Test that login endpoint exists and handles requests."""
340
- login_data = {
341
- "username": "test_user",
342
- "password": "test_password"
343
- }
344
-
345
- response = client.post("/api/v1/auth/login", json=login_data)
346
-
347
- # Should at least handle the request (even if returns 501)
348
- assert response.status_code in [200, 401, 422, 501]
349
 
350
- def test_token_validation(self):
351
- """Test token validation endpoint."""
352
- headers = {"Authorization": "Bearer fake_token"}
353
-
354
- response = client.get("/api/v1/auth/validate", headers=headers)
355
-
356
- # Should handle token validation
357
- assert response.status_code in [200, 401, 422, 501]
358
-
359
-
360
- class TestAPIInformation:
361
- """Test API information endpoints."""
362
 
363
- def test_root_endpoint(self):
364
- """Test root API endpoint."""
365
- response = client.get("/")
366
 
367
  assert response.status_code == 200
368
  data = response.json()
369
-
370
- assert "message" in data
371
- assert "version" in data
372
- assert "description" in data
373
 
374
- def test_api_info_endpoint(self):
375
- """Test API information endpoint."""
376
- response = client.get("/api/v1/info")
377
 
378
  assert response.status_code == 200
379
  data = response.json()
380
-
381
- assert "api" in data
382
  assert "agents" in data
383
- assert "data_sources" in data
384
- assert "formats" in data
385
-
386
- # Verify agent information
387
- agents = data["agents"]
388
- assert "investigator" in agents
389
- assert "analyst" in agents
390
- assert "reporter" in agents
391
-
392
- def test_openapi_schema(self):
393
- """Test OpenAPI schema generation."""
394
- response = client.get("/openapi.json")
395
 
396
  assert response.status_code == 200
397
- schema = response.json()
398
-
399
- assert "openapi" in schema
400
- assert "info" in schema
401
- assert "paths" in schema
402
- assert "components" in schema
403
-
404
-
405
- class TestErrorHandling:
406
- """Test API error handling."""
407
 
408
- def test_404_handling(self):
409
- """Test 404 error handling."""
410
- response = client.get("/api/v1/nonexistent-endpoint")
411
 
412
- assert response.status_code == 404
413
  data = response.json()
414
-
415
- assert "status" in data
416
- assert data["status"] == "error"
417
-
418
- def test_method_not_allowed(self):
419
- """Test 405 method not allowed."""
420
- response = client.patch("/health/") # Wrong method
421
-
422
- assert response.status_code == 405
423
-
424
- def test_large_payload_handling(self):
425
- """Test handling of large payloads."""
426
- large_data = {"data": "x" * 10000} # 10KB payload
427
-
428
- response = client.post("/api/v1/investigations/start", json=large_data)
429
-
430
- # Should handle gracefully (422 for validation, 413 for too large)
431
- assert response.status_code in [413, 422]
432
 
433
 
434
- class TestCORSHandling:
435
- """Test CORS configuration."""
436
-
437
- def test_cors_preflight(self):
438
- """Test CORS preflight request."""
439
- headers = {
440
- "Origin": "http://localhost:3000",
441
- "Access-Control-Request-Method": "POST",
442
- "Access-Control-Request-Headers": "Content-Type"
443
- }
444
-
445
- response = client.options("/api/v1/investigations/start", headers=headers)
446
-
447
- # Should handle CORS preflight
448
- assert response.status_code in [200, 204]
449
 
450
- def test_cors_headers_present(self):
451
- """Test that CORS headers are present in responses."""
452
- headers = {"Origin": "http://localhost:3000"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
- response = client.get("/health/", headers=headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
455
 
456
- # Check for CORS headers (may not be present in test environment)
457
  assert response.status_code == 200
 
 
458
 
459
 
460
- class TestSecurityHeaders:
461
- """Test security headers and middleware."""
462
 
463
- def test_security_headers_present(self):
464
- """Test that security headers are present."""
465
- response = client.get("/")
466
-
467
- # Common security headers that should be present
468
- headers = response.headers
469
-
470
- # These might be added by middleware
471
- # assert "X-Content-Type-Options" in headers
472
- # assert "X-Frame-Options" in headers
473
-
474
- # At minimum, should have content-type
475
- assert "content-type" in headers
476
 
477
- def test_trusted_host_validation(self):
478
- """Test trusted host middleware."""
479
- headers = {"Host": "malicious.example.com"}
480
-
481
- # Should be handled by TrustedHostMiddleware
482
- response = client.get("/", headers=headers)
483
-
484
- # Should either accept or reject based on configuration
485
- assert response.status_code in [200, 400, 403]
486
-
487
-
488
- @pytest.mark.integration
489
- class TestFullAPIWorkflow:
490
- """Test complete API workflows."""
491
-
492
- @pytest.mark.asyncio
493
- async def test_investigation_to_report_workflow(self, async_client, mock_agents):
494
- """Test complete workflow from investigation to report."""
495
- # Step 1: Start investigation
496
- investigation_data = {
497
- "query": "Test investigation workflow",
498
- "priority": "medium"
499
- }
500
-
501
- response = await async_client.post(
502
- "/api/v1/investigations/start",
503
- json=investigation_data
504
  )
505
 
506
- if response.status_code == 202:
507
- data = response.json()
508
- investigation_id = data.get("investigation_id", "test-123")
509
-
510
- # Step 2: Check status (simulate)
511
- status_response = await async_client.get(
512
- f"/api/v1/investigations/{investigation_id}/status"
513
- )
514
-
515
- # Step 3: Generate report
516
- report_data = {
517
- "type": "investigation",
518
- "investigation_id": investigation_id,
519
- "format": "markdown"
520
- }
521
-
522
- report_response = await async_client.post(
523
- "/api/v1/reports/generate",
524
- json=report_data
525
- )
526
-
527
- # Should complete workflow successfully
528
- assert report_response.status_code in [200, 202]
529
-
530
-
531
- class TestPerformanceAndLimits:
532
- """Test API performance and limits."""
533
 
534
- def test_concurrent_requests(self):
535
- """Test handling of concurrent requests."""
536
- import threading
537
- import time
538
-
539
- results = []
540
-
541
- def make_request():
542
- response = client.get("/health/")
543
- results.append(response.status_code)
544
-
545
- # Create 5 concurrent requests
546
- threads = []
547
- for _ in range(5):
548
- thread = threading.Thread(target=make_request)
549
- threads.append(thread)
550
- thread.start()
551
-
552
- # Wait for all to complete
553
- for thread in threads:
554
- thread.join()
555
 
556
- # All should succeed
557
- assert all(status == 200 for status in results)
558
- assert len(results) == 5
559
 
560
- def test_response_time_reasonable(self):
561
- """Test that response times are reasonable."""
562
- import time
563
-
564
- start_time = time.time()
565
- response = client.get("/health/")
566
- end_time = time.time()
567
-
568
- response_time = end_time - start_time
569
 
570
  assert response.status_code == 200
571
- assert response_time < 2.0 # Should respond within 2 seconds
572
-
573
-
574
- if __name__ == "__main__":
575
- pytest.main([__file__, "-v"])
 
1
+ """Integration tests for API endpoints."""
 
 
 
 
 
 
 
2
  import pytest
 
 
 
 
 
3
  from fastapi.testclient import TestClient
4
+ from unittest.mock import MagicMock, patch, AsyncMock
5
+ import json
6
+ from datetime import datetime
 
 
 
 
 
 
7
 
8
+ from src.api.app import create_app
9
+ from src.models.user import User
10
+ from src.models.investigation import Investigation, InvestigationStatus
11
+ from src.core.config import get_settings
 
12
 
13
 
14
+ class TestAuthEndpoints:
15
+ """Test authentication endpoints."""
16
+
17
+ @pytest.fixture
18
+ def client(self):
19
+ """Create test client."""
20
+ app = create_app()
21
+ return TestClient(app)
22
+
23
+ @pytest.fixture
24
+ def mock_user(self):
25
+ """Create mock user."""
26
+ return User(
27
+ id="test-user-id",
28
+ email="[email protected]",
29
+ hashed_password="$2b$12$hashed_password_here",
30
+ is_active=True,
31
+ role="user"
32
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ def test_register_success(self, client, mock_db):
35
+ """Test successful user registration."""
36
+ with patch("src.api.routes.auth.get_db", return_value=mock_db):
37
+ response = client.post(
38
+ "/api/v1/auth/register",
39
+ json={
40
+ "email": "[email protected]",
41
+ "password": "SecurePassword123!",
42
+ "full_name": "New User"
43
+ }
44
+ )
45
 
46
+ assert response.status_code == 201
47
  data = response.json()
48
+ assert data["email"] == "[email protected]"
49
+ assert "id" in data
50
+ assert "password" not in data
51
+
52
+ def test_register_duplicate_email(self, client, mock_db):
53
+ """Test registration with duplicate email."""
54
+ # Mock existing user
55
+ mock_db.execute.return_value.scalar_one_or_none.return_value = MagicMock()
56
+
57
+ with patch("src.api.routes.auth.get_db", return_value=mock_db):
58
+ response = client.post(
59
+ "/api/v1/auth/register",
60
+ json={
61
+ "email": "[email protected]",
62
+ "password": "Password123!"
63
+ }
64
+ )
65
 
66
+ assert response.status_code == 400
67
+ assert "already registered" in response.json()["detail"]
 
 
 
68
 
69
+ def test_login_success(self, client, mock_db, mock_user):
70
+ """Test successful login."""
71
+ # Mock authentication
72
+ with patch("src.api.auth.authenticate_user", return_value=mock_user):
73
+ response = client.post(
74
+ "/api/v1/auth/login",
75
+ data={
76
+ "username": "[email protected]",
77
+ "password": "correctpassword"
78
+ }
79
+ )
80
 
81
  assert response.status_code == 200
82
  data = response.json()
83
+ assert "access_token" in data
84
+ assert "refresh_token" in data
85
+ assert data["token_type"] == "bearer"
86
+
87
+ def test_login_invalid_credentials(self, client, mock_db):
88
+ """Test login with invalid credentials."""
89
+ with patch("src.api.auth.authenticate_user", return_value=None):
90
+ response = client.post(
91
+ "/api/v1/auth/login",
92
+ data={
93
+ "username": "[email protected]",
94
+ "password": "wrongpassword"
95
+ }
96
+ )
97
 
98
+ assert response.status_code == 401
99
+ assert "Invalid credentials" in response.json()["detail"]
 
100
 
101
+ def test_refresh_token_success(self, client, mock_db, mock_user):
102
+ """Test token refresh."""
103
+ # Create valid refresh token
104
+ with patch("src.api.auth.create_refresh_token") as mock_create_refresh:
105
+ mock_create_refresh.return_value = "valid_refresh_token"
106
+
107
+ with patch("src.api.auth.verify_token") as mock_verify:
108
+ mock_verify.return_value = {"sub": mock_user.id, "type": "refresh"}
109
+
110
+ with patch("src.api.routes.auth.get_db", return_value=mock_db):
111
+ mock_db.execute.return_value.scalar_one_or_none.return_value = mock_user
112
+
113
+ response = client.post(
114
+ "/api/v1/auth/refresh",
115
+ json={"refresh_token": "valid_refresh_token"}
116
+ )
117
 
118
  assert response.status_code == 200
119
  data = response.json()
120
+ assert "access_token" in data
121
+ assert "refresh_token" in data
122
 
123
+ def test_logout_success(self, client, authenticated_headers):
124
+ """Test logout."""
125
+ response = client.post(
126
+ "/api/v1/auth/logout",
127
+ headers=authenticated_headers
128
+ )
129
 
130
+ assert response.status_code == 200
131
+ assert response.json()["message"] == "Logged out successfully"
132
+
133
+ def test_change_password(self, client, authenticated_headers, mock_db, mock_user):
134
+ """Test password change."""
135
+ with patch("src.api.auth.verify_password", return_value=True):
136
+ with patch("src.api.routes.auth.get_current_user", return_value=mock_user):
137
+ response = client.post(
138
+ "/api/v1/auth/change-password",
139
+ headers=authenticated_headers,
140
+ json={
141
+ "old_password": "oldpassword",
142
+ "new_password": "NewSecurePassword123!"
143
+ }
144
+ )
145
 
146
+ assert response.status_code == 200
147
+ assert "Password changed successfully" in response.json()["message"]
148
 
149
 
150
  class TestInvestigationEndpoints:
151
  """Test investigation endpoints."""
152
 
153
+ @pytest.fixture
154
+ def client(self):
155
+ """Create test client."""
156
+ app = create_app()
157
+ return TestClient(app)
158
+
159
+ @pytest.fixture
160
+ def mock_investigation(self):
161
+ """Create mock investigation."""
162
+ return Investigation(
163
+ id="inv-123",
164
+ user_id="user-123",
165
+ title="Test Investigation",
166
+ description="Testing anomaly detection",
167
+ target_entity="Ministry of Health",
168
+ status=InvestigationStatus.PENDING,
169
+ created_at=datetime.utcnow()
170
  )
171
+
172
+ def test_create_investigation(self, client, authenticated_headers, mock_db):
173
+ """Test creating new investigation."""
174
+ with patch("src.api.routes.investigations.get_db", return_value=mock_db):
175
+ # Mock the agent execution
176
+ with patch("src.agents.abaporu.MasterAgent.execute") as mock_execute:
177
+ mock_execute.return_value = AsyncMock(
178
+ status="completed",
179
+ results={"anomalies": [], "summary": "No anomalies found"}
180
+ )
181
+
182
+ response = client.post(
183
+ "/api/v1/investigations",
184
+ headers=authenticated_headers,
185
+ json={
186
+ "title": "Contract Analysis",
187
+ "description": "Analyze contracts for Ministry of Health",
188
+ "target_entity": "Ministry of Health",
189
+ "parameters": {
190
+ "year": 2024,
191
+ "min_value": 100000
192
+ }
193
+ }
194
+ )
195
+
196
+ assert response.status_code == 201
197
  data = response.json()
198
+ assert data["title"] == "Contract Analysis"
199
+ assert "id" in data
200
+ assert data["status"] == "pending"
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ def test_list_investigations(self, client, authenticated_headers, mock_db):
 
203
  """Test listing investigations."""
204
+ # Mock investigations
205
+ mock_investigations = [
206
+ MagicMock(id="inv-1", title="Investigation 1"),
207
+ MagicMock(id="inv-2", title="Investigation 2")
208
+ ]
209
+
210
+ mock_db.execute.return_value.scalars.return_value.all.return_value = mock_investigations
211
+
212
+ with patch("src.api.routes.investigations.get_db", return_value=mock_db):
213
+ response = client.get(
214
+ "/api/v1/investigations",
215
+ headers=authenticated_headers
216
+ )
217
 
218
  assert response.status_code == 200
219
  data = response.json()
220
+ assert len(data) == 2
221
+ assert data[0]["title"] == "Investigation 1"
222
+
223
+ def test_get_investigation_by_id(self, client, authenticated_headers, mock_db, mock_investigation):
224
+ """Test getting specific investigation."""
225
+ mock_db.execute.return_value.scalar_one_or_none.return_value = mock_investigation
226
+
227
+ with patch("src.api.routes.investigations.get_db", return_value=mock_db):
228
+ response = client.get(
229
+ f"/api/v1/investigations/{mock_investigation.id}",
230
+ headers=authenticated_headers
231
+ )
232
 
233
+ assert response.status_code == 200
234
+ data = response.json()
235
+ assert data["id"] == mock_investigation.id
236
+ assert data["title"] == mock_investigation.title
237
+
238
+ def test_get_investigation_not_found(self, client, authenticated_headers, mock_db):
239
+ """Test getting non-existent investigation."""
240
+ mock_db.execute.return_value.scalar_one_or_none.return_value = None
241
+
242
+ with patch("src.api.routes.investigations.get_db", return_value=mock_db):
243
+ response = client.get(
244
+ "/api/v1/investigations/non-existent-id",
245
+ headers=authenticated_headers
246
+ )
 
 
 
 
 
 
 
 
 
 
247
 
248
+ assert response.status_code == 404
249
+ assert "Investigation not found" in response.json()["detail"]
250
+
251
+ def test_investigation_websocket(self, client, authenticated_headers):
252
+ """Test investigation real-time updates via WebSocket."""
253
+ with client.websocket_connect(
254
+ "/api/v1/investigations/ws/inv-123",
255
+ headers=authenticated_headers
256
+ ) as websocket:
257
+ # Send initial message
258
+ websocket.send_json({"type": "subscribe"})
259
+
260
+ # Receive acknowledgment
261
+ data = websocket.receive_json()
262
+ assert data["type"] == "connected"
263
+ assert data["investigation_id"] == "inv-123"
264
 
265
 
266
  class TestAnalysisEndpoints:
267
  """Test analysis endpoints."""
268
 
269
+ @pytest.fixture
270
+ def client(self):
271
+ """Create test client."""
272
+ app = create_app()
273
+ return TestClient(app)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ def test_analyze_contracts(self, client, authenticated_headers):
276
+ """Test contract analysis endpoint."""
277
+ with patch("src.services.analysis_service.AnalysisService.analyze_contracts") as mock_analyze:
278
+ mock_analyze.return_value = {
279
+ "total_contracts": 100,
280
+ "anomalies_detected": 5,
281
+ "total_value": 10000000,
282
+ "risk_score": 0.7
283
+ }
284
+
285
+ response = client.post(
286
+ "/api/v1/analysis/contracts",
287
+ headers=authenticated_headers,
288
+ json={
289
+ "entity_code": "26000",
290
+ "year": 2024,
291
+ "min_value": 100000
292
+ }
293
+ )
294
 
295
+ assert response.status_code == 200
296
+ data = response.json()
297
+ assert data["total_contracts"] == 100
298
+ assert data["anomalies_detected"] == 5
299
 
300
+ def test_analyze_spending_patterns(self, client, authenticated_headers):
301
+ """Test spending pattern analysis."""
302
+ with patch("src.services.analysis_service.AnalysisService.analyze_spending") as mock_analyze:
303
+ mock_analyze.return_value = {
304
+ "patterns": [
305
+ {
306
+ "type": "seasonal",
307
+ "description": "High spending in Q4",
308
+ "confidence": 0.85
309
+ }
310
+ ],
311
+ "anomalies": []
312
+ }
313
+
314
+ response = client.post(
315
+ "/api/v1/analysis/spending-patterns",
316
+ headers=authenticated_headers,
317
+ json={
318
+ "entity_code": "26000",
319
+ "start_date": "2024-01-01",
320
+ "end_date": "2024-12-31"
321
+ }
322
+ )
323
 
324
  assert response.status_code == 200
325
  data = response.json()
326
+ assert len(data["patterns"]) == 1
327
+ assert data["patterns"][0]["type"] == "seasonal"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
+ def test_vendor_concentration_analysis(self, client, authenticated_headers):
330
+ """Test vendor concentration analysis."""
331
+ response = client.post(
332
+ "/api/v1/analysis/vendor-concentration",
333
+ headers=authenticated_headers,
334
+ json={
335
+ "entity_code": "26000",
336
+ "year": 2024,
337
+ "concentration_threshold": 0.7
338
+ }
339
+ )
340
 
341
  assert response.status_code == 200
342
  data = response.json()
343
+ assert "concentration_index" in data
344
+ assert "top_vendors" in data
 
345
 
346
 
347
+ class TestHealthEndpoints:
348
+ """Test health and monitoring endpoints."""
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ @pytest.fixture
351
+ def client(self):
352
+ """Create test client."""
353
+ app = create_app()
354
+ return TestClient(app)
 
 
 
 
 
 
 
355
 
356
+ def test_health_check(self, client):
357
+ """Test basic health check."""
358
+ response = client.get("/health")
359
 
360
  assert response.status_code == 200
361
  data = response.json()
362
+ assert data["status"] == "healthy"
363
+ assert "timestamp" in data
 
 
364
 
365
+ def test_detailed_health_check(self, client):
366
+ """Test detailed health check."""
367
+ response = client.get("/health/detailed")
368
 
369
  assert response.status_code == 200
370
  data = response.json()
371
+ assert "database" in data
372
+ assert "redis" in data
373
  assert "agents" in data
374
+
375
+ def test_metrics_endpoint(self, client):
376
+ """Test Prometheus metrics endpoint."""
377
+ response = client.get("/health/metrics")
 
 
 
 
 
 
 
 
378
 
379
  assert response.status_code == 200
380
+ assert response.headers["content-type"] == "text/plain; version=0.0.4; charset=utf-8"
381
+ assert "cidadao_ai_requests_total" in response.text
 
 
 
 
 
 
 
 
382
 
383
+ def test_metrics_json_endpoint(self, client):
384
+ """Test JSON metrics endpoint."""
385
+ response = client.get("/health/metrics/json")
386
 
387
+ assert response.status_code == 200
388
  data = response.json()
389
+ assert "requests" in data
390
+ assert "agents" in data
391
+ assert "anomalies" in data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
 
394
+ class TestReportEndpoints:
395
+ """Test report generation endpoints."""
396
+
397
+ @pytest.fixture
398
+ def client(self):
399
+ """Create test client."""
400
+ app = create_app()
401
+ return TestClient(app)
 
 
 
 
 
 
 
402
 
403
+ def test_generate_investigation_report(self, client, authenticated_headers):
404
+ """Test report generation for investigation."""
405
+ with patch("src.agents.tiradentes.ReporterAgent.generate_report") as mock_generate:
406
+ mock_generate.return_value = {
407
+ "title": "Investigation Report",
408
+ "summary": "5 anomalies detected",
409
+ "sections": [
410
+ {
411
+ "title": "Price Anomalies",
412
+ "content": "Found 3 contracts with unusual pricing"
413
+ }
414
+ ],
415
+ "recommendations": ["Review contract approval process"]
416
+ }
417
+
418
+ response = client.post(
419
+ "/api/v1/reports/investigation/inv-123",
420
+ headers=authenticated_headers,
421
+ json={
422
+ "format": "markdown",
423
+ "include_evidence": True
424
+ }
425
+ )
426
 
427
+ assert response.status_code == 200
428
+ data = response.json()
429
+ assert data["title"] == "Investigation Report"
430
+ assert len(data["sections"]) == 1
431
+
432
+ def test_export_report_pdf(self, client, authenticated_headers):
433
+ """Test PDF report export."""
434
+ with patch("src.services.report_service.ReportService.export_pdf") as mock_export:
435
+ mock_export.return_value = b"PDF content here"
436
+
437
+ response = client.get(
438
+ "/api/v1/reports/investigation/inv-123/export?format=pdf",
439
+ headers=authenticated_headers
440
+ )
441
 
 
442
  assert response.status_code == 200
443
+ assert response.headers["content-type"] == "application/pdf"
444
+ assert response.headers["content-disposition"] == 'attachment; filename="investigation_inv-123.pdf"'
445
 
446
 
447
+ class TestAuditEndpoints:
448
+ """Test audit trail endpoints."""
449
 
450
+ @pytest.fixture
451
+ def client(self):
452
+ """Create test client."""
453
+ app = create_app()
454
+ return TestClient(app)
 
 
 
 
 
 
 
 
455
 
456
+ def test_get_audit_logs(self, client, admin_headers):
457
+ """Test retrieving audit logs (admin only)."""
458
+ response = client.get(
459
+ "/api/v1/audit/logs?limit=50",
460
+ headers=admin_headers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  )
462
 
463
+ assert response.status_code == 200
464
+ data = response.json()
465
+ assert isinstance(data, list)
466
+ assert len(data) <= 50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
+ def test_get_audit_logs_unauthorized(self, client, authenticated_headers):
469
+ """Test audit logs require admin role."""
470
+ response = client.get(
471
+ "/api/v1/audit/logs",
472
+ headers=authenticated_headers
473
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
 
475
+ assert response.status_code == 403
476
+ assert "Insufficient permissions" in response.json()["detail"]
 
477
 
478
+ def test_export_audit_logs(self, client, admin_headers):
479
+ """Test exporting audit logs."""
480
+ response = client.get(
481
+ "/api/v1/audit/export?format=csv&start_date=2024-01-01",
482
+ headers=admin_headers
483
+ )
 
 
 
484
 
485
  assert response.status_code == 200
486
+ assert response.headers["content-type"] == "text/csv"
487
+ assert "attachment" in response.headers["content-disposition"]