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
- 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
|