anderson-ufrj commited on
Commit
dd1b2de
·
1 Parent(s): 43cf505

test(services): add service layer tests

Browse files

- Test analysis service functionality
- Test data service operations
- Test notification service
- Test investigation service
- Add contract analysis tests
- Test risk assessment calculations

Files changed (1) hide show
  1. tests/unit/test_services.py +486 -0
tests/unit/test_services.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for service layer components."""
2
+ import pytest
3
+ from unittest.mock import MagicMock, patch, AsyncMock
4
+ from datetime import datetime, timedelta
5
+ import pandas as pd
6
+ import numpy as np
7
+
8
+ from src.services.analysis_service import (
9
+ AnalysisService,
10
+ ContractAnalysis,
11
+ SpendingAnalysis,
12
+ VendorAnalysis,
13
+ RiskAssessment
14
+ )
15
+ from src.services.data_service import (
16
+ DataService,
17
+ DataSource,
18
+ DataFilter,
19
+ DataAggregation,
20
+ DataQuality
21
+ )
22
+ from src.services.notification_service import (
23
+ NotificationService,
24
+ NotificationType,
25
+ NotificationChannel,
26
+ NotificationPriority,
27
+ NotificationTemplate
28
+ )
29
+ from src.services.investigation_service import (
30
+ InvestigationService,
31
+ InvestigationRequest,
32
+ InvestigationPlan,
33
+ InvestigationResult
34
+ )
35
+
36
+
37
+ class TestAnalysisService:
38
+ """Test analysis service functionality."""
39
+
40
+ @pytest.fixture
41
+ def analysis_service(self):
42
+ """Create analysis service instance."""
43
+ return AnalysisService()
44
+
45
+ @pytest.fixture
46
+ def sample_contracts(self):
47
+ """Create sample contract data."""
48
+ return pd.DataFrame({
49
+ 'contract_id': [f'CTR-{i:03d}' for i in range(100)],
50
+ 'value': np.random.lognormal(11, 1.5, 100), # Log-normal distribution
51
+ 'vendor_id': np.random.choice(['V001', 'V002', 'V003', 'V004', 'V005'], 100),
52
+ 'date': pd.date_range('2024-01-01', periods=100, freq='D'),
53
+ 'category': np.random.choice(['IT', 'Medical', 'Construction'], 100),
54
+ 'duration_days': np.random.randint(30, 365, 100)
55
+ })
56
+
57
+ @pytest.mark.asyncio
58
+ async def test_contract_analysis(self, analysis_service, sample_contracts):
59
+ """Test contract analysis functionality."""
60
+ analysis = ContractAnalysis()
61
+
62
+ result = await analysis.analyze_contracts(sample_contracts)
63
+
64
+ assert result is not None
65
+ assert 'summary_stats' in result
66
+ assert 'anomalies' in result
67
+ assert 'risk_score' in result
68
+
69
+ # Check summary statistics
70
+ stats = result['summary_stats']
71
+ assert stats['total_contracts'] == 100
72
+ assert stats['total_value'] > 0
73
+ assert stats['avg_value'] > 0
74
+ assert stats['median_value'] > 0
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_anomaly_detection_in_contracts(self, analysis_service, sample_contracts):
78
+ """Test anomaly detection in contract analysis."""
79
+ # Add anomalous contracts
80
+ anomalous = sample_contracts.copy()
81
+ anomalous.loc[0, 'value'] = anomalous['value'].mean() * 10 # 10x average
82
+ anomalous.loc[1, 'duration_days'] = 1 # Very short duration
83
+
84
+ analysis = ContractAnalysis()
85
+ result = await analysis.analyze_contracts(anomalous)
86
+
87
+ anomalies = result['anomalies']
88
+ assert len(anomalies) >= 2
89
+
90
+ # Check anomaly details
91
+ assert any(a['type'] == 'price_anomaly' for a in anomalies)
92
+ assert any(a['type'] == 'duration_anomaly' for a in anomalies)
93
+
94
+ @pytest.mark.asyncio
95
+ async def test_spending_pattern_analysis(self, analysis_service):
96
+ """Test spending pattern analysis."""
97
+ # Create spending data with patterns
98
+ dates = pd.date_range('2024-01-01', '2024-12-31', freq='D')
99
+ spending_data = pd.DataFrame({
100
+ 'date': dates,
101
+ 'amount': [
102
+ 100000 * (1 + 0.5 * np.sin(2 * np.pi * i / 30)) # Monthly pattern
103
+ + 50000 * (1 if d.month == 12 else 0) # Year-end spike
104
+ + np.random.normal(0, 10000) # Noise
105
+ for i, d in enumerate(dates)
106
+ ],
107
+ 'department': np.random.choice(['Health', 'Education', 'Infrastructure'], len(dates))
108
+ })
109
+
110
+ analysis = SpendingAnalysis()
111
+ patterns = await analysis.detect_patterns(spending_data)
112
+
113
+ assert 'seasonal_patterns' in patterns
114
+ assert 'trend' in patterns
115
+ assert 'anomalous_periods' in patterns
116
+
117
+ # Should detect year-end spike
118
+ anomalous = patterns['anomalous_periods']
119
+ assert any(p['period'].month == 12 for p in anomalous)
120
+
121
+ @pytest.mark.asyncio
122
+ async def test_vendor_concentration_analysis(self, analysis_service, sample_contracts):
123
+ """Test vendor concentration analysis."""
124
+ # Create concentrated vendor scenario
125
+ concentrated = sample_contracts.copy()
126
+ concentrated.loc[:70, 'vendor_id'] = 'V001' # 70% to one vendor
127
+
128
+ analysis = VendorAnalysis()
129
+ result = await analysis.analyze_concentration(concentrated)
130
+
131
+ assert 'concentration_index' in result
132
+ assert 'top_vendors' in result
133
+ assert 'risk_level' in result
134
+
135
+ # Should detect high concentration
136
+ assert result['concentration_index'] > 0.7
137
+ assert result['risk_level'] == 'high'
138
+ assert result['top_vendors'][0]['vendor_id'] == 'V001'
139
+ assert result['top_vendors'][0]['percentage'] > 0.7
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_risk_assessment(self, analysis_service, sample_contracts):
143
+ """Test comprehensive risk assessment."""
144
+ assessment = RiskAssessment()
145
+
146
+ # Add risk factors
147
+ risk_contracts = sample_contracts.copy()
148
+ risk_contracts.loc[0, 'value'] = risk_contracts['value'].mean() * 20
149
+ risk_contracts.loc[:60, 'vendor_id'] = 'V001' # Vendor concentration
150
+
151
+ risk_score = await assessment.calculate_risk(
152
+ contracts=risk_contracts,
153
+ historical_data=sample_contracts
154
+ )
155
+
156
+ assert isinstance(risk_score, dict)
157
+ assert 'overall_risk' in risk_score
158
+ assert 'risk_factors' in risk_score
159
+ assert 'recommendations' in risk_score
160
+
161
+ assert risk_score['overall_risk'] > 0.7 # High risk
162
+ assert len(risk_score['risk_factors']) >= 2
163
+
164
+
165
+ class TestDataService:
166
+ """Test data service functionality."""
167
+
168
+ @pytest.fixture
169
+ def data_service(self):
170
+ """Create data service instance."""
171
+ return DataService()
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_data_source_registration(self, data_service):
175
+ """Test registering data sources."""
176
+ source = DataSource(
177
+ id="transparency_api",
178
+ name="Portal da Transparência",
179
+ type="api",
180
+ config={
181
+ "base_url": "https://api.portaltransparencia.gov.br",
182
+ "auth_required": True
183
+ }
184
+ )
185
+
186
+ await data_service.register_source(source)
187
+
188
+ # Verify registration
189
+ registered = await data_service.get_source("transparency_api")
190
+ assert registered is not None
191
+ assert registered.name == "Portal da Transparência"
192
+
193
+ @pytest.mark.asyncio
194
+ async def test_data_filtering(self, data_service):
195
+ """Test data filtering capabilities."""
196
+ # Sample data
197
+ data = pd.DataFrame({
198
+ 'entity': ['MinHealth', 'MinEdu', 'MinHealth', 'MinInfra'],
199
+ 'value': [100000, 200000, 150000, 300000],
200
+ 'date': pd.to_datetime(['2024-01-01', '2024-02-01', '2024-03-01', '2024-04-01']),
201
+ 'status': ['active', 'active', 'cancelled', 'active']
202
+ })
203
+
204
+ # Apply filters
205
+ filters = DataFilter(
206
+ entity="MinHealth",
207
+ status="active",
208
+ date_range=("2024-01-01", "2024-12-31")
209
+ )
210
+
211
+ filtered = await data_service.apply_filters(data, filters)
212
+
213
+ assert len(filtered) == 1
214
+ assert filtered.iloc[0]['entity'] == 'MinHealth'
215
+ assert filtered.iloc[0]['status'] == 'active'
216
+
217
+ @pytest.mark.asyncio
218
+ async def test_data_aggregation(self, data_service):
219
+ """Test data aggregation functionality."""
220
+ data = pd.DataFrame({
221
+ 'department': ['Health', 'Health', 'Education', 'Education'],
222
+ 'category': ['IT', 'Medical', 'IT', 'Books'],
223
+ 'amount': [100000, 200000, 150000, 50000]
224
+ })
225
+
226
+ aggregation = DataAggregation(
227
+ group_by=['department'],
228
+ aggregations={
229
+ 'amount': ['sum', 'mean', 'count'],
230
+ }
231
+ )
232
+
233
+ result = await data_service.aggregate_data(data, aggregation)
234
+
235
+ assert len(result) == 2 # Two departments
236
+ assert 'amount_sum' in result.columns
237
+ assert 'amount_mean' in result.columns
238
+ assert 'amount_count' in result.columns
239
+
240
+ health_row = result[result['department'] == 'Health'].iloc[0]
241
+ assert health_row['amount_sum'] == 300000
242
+ assert health_row['amount_count'] == 2
243
+
244
+ @pytest.mark.asyncio
245
+ async def test_data_quality_assessment(self, data_service):
246
+ """Test data quality assessment."""
247
+ # Create data with quality issues
248
+ data = pd.DataFrame({
249
+ 'id': [1, 2, 3, None, 5], # Missing value
250
+ 'value': [100, 200, -50, 300, 1e9], # Negative and outlier
251
+ 'date': ['2024-01-01', '2024-02-01', 'invalid', '2024-04-01', '2024-05-01'],
252
+ 'duplicate': [1, 1, 2, 3, 4] # Duplicate values
253
+ })
254
+
255
+ quality = DataQuality()
256
+ assessment = await quality.assess_quality(data)
257
+
258
+ assert 'completeness' in assessment
259
+ assert 'validity' in assessment
260
+ assert 'consistency' in assessment
261
+ assert 'issues' in assessment
262
+
263
+ # Should detect issues
264
+ assert assessment['completeness'] < 1.0 # Missing values
265
+ assert len(assessment['issues']) > 0
266
+ assert any(issue['type'] == 'missing_value' for issue in assessment['issues'])
267
+ assert any(issue['type'] == 'invalid_value' for issue in assessment['issues'])
268
+
269
+
270
+ class TestNotificationService:
271
+ """Test notification service functionality."""
272
+
273
+ @pytest.fixture
274
+ def notification_service(self):
275
+ """Create notification service instance."""
276
+ return NotificationService()
277
+
278
+ @pytest.mark.asyncio
279
+ async def test_send_notification(self, notification_service):
280
+ """Test sending notifications."""
281
+ # Mock notification channels
282
+ with patch.object(notification_service, '_send_email') as mock_email:
283
+ mock_email.return_value = True
284
+
285
+ result = await notification_service.send_notification(
286
+ type=NotificationType.ANOMALY_DETECTED,
287
+ channel=NotificationChannel.EMAIL,
288
+ recipient="[email protected]",
289
+ data={
290
+ "anomaly_count": 5,
291
+ "severity": "high",
292
+ "investigation_id": "inv-123"
293
+ }
294
+ )
295
+
296
+ assert result is True
297
+ mock_email.assert_called_once()
298
+
299
+ @pytest.mark.asyncio
300
+ async def test_notification_templates(self, notification_service):
301
+ """Test notification template rendering."""
302
+ template = NotificationTemplate(
303
+ id="anomaly_alert",
304
+ type=NotificationType.ANOMALY_DETECTED,
305
+ subject="🚨 {anomaly_count} Anomalies Detected",
306
+ body="""
307
+ Investigation: {investigation_id}
308
+ Severity: {severity}
309
+ Anomalies Found: {anomaly_count}
310
+
311
+ Please review the findings immediately.
312
+ """
313
+ )
314
+
315
+ rendered = await notification_service.render_template(
316
+ template,
317
+ data={
318
+ "anomaly_count": 3,
319
+ "severity": "medium",
320
+ "investigation_id": "inv-456"
321
+ }
322
+ )
323
+
324
+ assert "3 Anomalies Detected" in rendered['subject']
325
+ assert "inv-456" in rendered['body']
326
+ assert "medium" in rendered['body']
327
+
328
+ @pytest.mark.asyncio
329
+ async def test_notification_priority_queue(self, notification_service):
330
+ """Test notification priority queuing."""
331
+ # Queue notifications with different priorities
332
+ notifications = [
333
+ {
334
+ "type": NotificationType.SYSTEM_ALERT,
335
+ "priority": NotificationPriority.LOW,
336
+ "data": {"message": "Low priority"}
337
+ },
338
+ {
339
+ "type": NotificationType.ANOMALY_DETECTED,
340
+ "priority": NotificationPriority.CRITICAL,
341
+ "data": {"message": "Critical anomaly"}
342
+ },
343
+ {
344
+ "type": NotificationType.REPORT_READY,
345
+ "priority": NotificationPriority.MEDIUM,
346
+ "data": {"message": "Report ready"}
347
+ }
348
+ ]
349
+
350
+ for notif in notifications:
351
+ await notification_service.queue_notification(**notif)
352
+
353
+ # Process queue - critical should be first
354
+ processed = await notification_service.process_queue()
355
+
356
+ assert processed[0]['priority'] == NotificationPriority.CRITICAL
357
+ assert processed[-1]['priority'] == NotificationPriority.LOW
358
+
359
+ @pytest.mark.asyncio
360
+ async def test_notification_rate_limiting(self, notification_service):
361
+ """Test notification rate limiting."""
362
+ recipient = "[email protected]"
363
+
364
+ # Send multiple notifications
365
+ for i in range(10):
366
+ await notification_service.send_notification(
367
+ type=NotificationType.ANOMALY_DETECTED,
368
+ channel=NotificationChannel.EMAIL,
369
+ recipient=recipient,
370
+ data={"count": i}
371
+ )
372
+
373
+ # Check rate limit
374
+ stats = await notification_service.get_recipient_stats(recipient)
375
+ assert stats['notifications_sent'] <= notification_service.rate_limit_per_hour
376
+
377
+
378
+ class TestInvestigationService:
379
+ """Test investigation service functionality."""
380
+
381
+ @pytest.fixture
382
+ def investigation_service(self):
383
+ """Create investigation service instance."""
384
+ return InvestigationService()
385
+
386
+ @pytest.mark.asyncio
387
+ async def test_create_investigation_plan(self, investigation_service):
388
+ """Test creating investigation plan."""
389
+ request = InvestigationRequest(
390
+ id="req-123",
391
+ query="Analyze health ministry contracts for overpricing",
392
+ parameters={
393
+ "entity": "Ministry of Health",
394
+ "period": "2024",
395
+ "focus": "price_anomalies"
396
+ }
397
+ )
398
+
399
+ plan = await investigation_service.create_plan(request)
400
+
401
+ assert isinstance(plan, InvestigationPlan)
402
+ assert len(plan.steps) > 0
403
+ assert any(step.agent_type == "investigator" for step in plan.steps)
404
+ assert any(step.agent_type == "analyst" for step in plan.steps)
405
+ assert plan.estimated_duration > 0
406
+
407
+ @pytest.mark.asyncio
408
+ async def test_execute_investigation(self, investigation_service):
409
+ """Test investigation execution."""
410
+ # Mock agent responses
411
+ with patch('src.agents.abaporu.MasterAgent.execute') as mock_execute:
412
+ mock_execute.return_value = AsyncMock(
413
+ status="completed",
414
+ result={
415
+ "anomalies": [
416
+ {"type": "price", "severity": 0.8},
417
+ {"type": "vendor", "severity": 0.6}
418
+ ],
419
+ "summary": "Found 2 significant anomalies"
420
+ }
421
+ )
422
+
423
+ request = InvestigationRequest(
424
+ id="inv-exec-123",
425
+ query="Test investigation",
426
+ parameters={}
427
+ )
428
+
429
+ result = await investigation_service.execute_investigation(request)
430
+
431
+ assert isinstance(result, InvestigationResult)
432
+ assert result.status == "completed"
433
+ assert len(result.findings['anomalies']) == 2
434
+ assert result.confidence_score > 0
435
+
436
+ @pytest.mark.asyncio
437
+ async def test_investigation_progress_tracking(self, investigation_service):
438
+ """Test tracking investigation progress."""
439
+ investigation_id = "track-123"
440
+
441
+ # Update progress
442
+ await investigation_service.update_progress(
443
+ investigation_id,
444
+ step="data_collection",
445
+ progress=0.5,
446
+ message="Collected 50% of contract data"
447
+ )
448
+
449
+ await investigation_service.update_progress(
450
+ investigation_id,
451
+ step="analysis",
452
+ progress=0.3,
453
+ message="Analyzing patterns"
454
+ )
455
+
456
+ # Get overall progress
457
+ progress = await investigation_service.get_progress(investigation_id)
458
+
459
+ assert progress['overall_progress'] > 0
460
+ assert 'data_collection' in progress['steps']
461
+ assert progress['steps']['data_collection']['progress'] == 0.5
462
+
463
+ @pytest.mark.asyncio
464
+ async def test_investigation_caching(self, investigation_service):
465
+ """Test investigation result caching."""
466
+ request = InvestigationRequest(
467
+ id="cache-123",
468
+ query="Cached investigation",
469
+ parameters={"entity": "test", "use_cache": True}
470
+ )
471
+
472
+ # First execution
473
+ with patch('src.agents.abaporu.MasterAgent.execute') as mock_execute:
474
+ mock_execute.return_value = AsyncMock(
475
+ status="completed",
476
+ result={"data": "first_execution"}
477
+ )
478
+
479
+ result1 = await investigation_service.execute_investigation(request)
480
+ assert mock_execute.call_count == 1
481
+
482
+ # Second execution should use cache
483
+ result2 = await investigation_service.execute_investigation(request)
484
+
485
+ assert result1.findings == result2.findings
486
+ assert result2.from_cache is True