""" Unit tests for Pydantic models with validation and security tests """ import pytest from datetime import datetime, timezone from pydantic import ValidationError from models import ( ReliabilityEvent, EventSeverity, HealingPolicy, HealingAction, PolicyCondition, AnomalyResult, ForecastResult ) class TestReliabilityEventValidation: """Test ReliabilityEvent validation""" def test_valid_event_creation(self): """Test creating a valid event""" event = ReliabilityEvent( component="api-service", latency_p99=150.0, error_rate=0.05, throughput=1000.0, cpu_util=0.7, memory_util=0.6 ) assert event.component == "api-service" assert event.latency_p99 == 150.0 assert event.error_rate == 0.05 assert isinstance(event.timestamp, datetime) assert event.severity == EventSeverity.LOW def test_component_validation_valid(self): """Test valid component IDs""" valid_ids = ["api-service", "auth-service", "payment-service-v2", "db-01"] for component_id in valid_ids: event = ReliabilityEvent( component=component_id, latency_p99=100.0, error_rate=0.01, throughput=1000.0 ) assert event.component == component_id def test_component_validation_invalid(self): """Test invalid component IDs are rejected""" invalid_ids = [ "API-SERVICE", # Uppercase "api_service", # Underscore "api service", # Space "api@service", # Special char "", # Empty ] for component_id in invalid_ids: with pytest.raises(ValidationError) as exc_info: ReliabilityEvent( component=component_id, latency_p99=100.0, error_rate=0.01, throughput=1000.0 ) assert "component" in str(exc_info.value).lower() def test_latency_bounds(self): """Test latency validation bounds""" # Valid latency event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.01, throughput=1000.0 ) assert event.latency_p99 == 100.0 # Negative latency should fail with pytest.raises(ValidationError): ReliabilityEvent( component="test-service", latency_p99=-10.0, error_rate=0.01, throughput=1000.0 ) # Extremely high latency should fail (> 5 minutes) with pytest.raises(ValidationError): ReliabilityEvent( component="test-service", latency_p99=400000.0, # > 300000ms limit error_rate=0.01, throughput=1000.0 ) def test_error_rate_bounds(self): """Test error rate validation""" # Valid error rate event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.5, throughput=1000.0 ) assert event.error_rate == 0.5 # Negative error rate should fail with pytest.raises(ValidationError): ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=-0.1, throughput=1000.0 ) # Error rate > 1 should fail with pytest.raises(ValidationError): ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=1.5, throughput=1000.0 ) def test_resource_utilization_bounds(self): """Test CPU and memory utilization bounds""" # Valid utilization event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.01, throughput=1000.0, cpu_util=0.85, memory_util=0.75 ) assert event.cpu_util == 0.85 assert event.memory_util == 0.75 # CPU > 1 should fail with pytest.raises(ValidationError): ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.01, throughput=1000.0, cpu_util=1.5 ) # Memory < 0 should fail with pytest.raises(ValidationError): ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.01, throughput=1000.0, memory_util=-0.1 ) class TestEventFingerprint: """Test event fingerprint generation (SHA-256)""" def test_fingerprint_is_sha256(self): """Test that fingerprint uses SHA-256 (64 hex chars)""" event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) # SHA-256 produces 64 hex characters assert len(event.fingerprint) == 64 assert all(c in '0123456789abcdef' for c in event.fingerprint) def test_fingerprint_deterministic(self): """Test that same inputs produce same fingerprint""" event1 = ReliabilityEvent( component="test-service", service_mesh="default", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) event2 = ReliabilityEvent( component="test-service", service_mesh="default", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) # Should produce same fingerprint (timestamp not included) assert event1.fingerprint == event2.fingerprint def test_fingerprint_different_for_different_events(self): """Test that different events produce different fingerprints""" event1 = ReliabilityEvent( component="service-1", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) event2 = ReliabilityEvent( component="service-2", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) assert event1.fingerprint != event2.fingerprint def test_fingerprint_not_md5(self): """Test that fingerprint is NOT MD5 (security fix verification)""" event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) # MD5 produces 32 hex chars, SHA-256 produces 64 assert len(event.fingerprint) != 32 assert len(event.fingerprint) == 64 class TestEventImmutability: """Test that events are immutable (frozen)""" def test_event_is_frozen(self): """Test that ReliabilityEvent is frozen""" event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) # Attempting to modify should raise ValidationError with pytest.raises(ValidationError): event.latency_p99 = 200.0 def test_model_copy_with_update(self): """Test that model_copy creates new instance with updates""" event1 = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0, severity=EventSeverity.LOW ) # Create modified copy event2 = event1.model_copy(update={'severity': EventSeverity.HIGH}) # Original unchanged assert event1.severity == EventSeverity.LOW # Copy updated assert event2.severity == EventSeverity.HIGH # Other fields same assert event2.component == event1.component assert event2.latency_p99 == event1.latency_p99 class TestDependencyValidation: """Test dependency cycle detection""" def test_valid_dependencies(self): """Test valid dependency configuration""" event = ReliabilityEvent( component="api-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0, upstream_deps=["auth-service", "database"], downstream_deps=["frontend", "mobile-app"] ) assert "auth-service" in event.upstream_deps assert "frontend" in event.downstream_deps def test_circular_dependency_detected(self): """Test that circular dependencies are detected""" with pytest.raises(ValidationError) as exc_info: ReliabilityEvent( component="api-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0, upstream_deps=["auth-service", "database"], downstream_deps=["database", "frontend"] # 'database' in both ) error_msg = str(exc_info.value).lower() assert "circular" in error_msg or "database" in error_msg def test_dependency_name_validation(self): """Test that dependency names follow same rules as component IDs""" # Valid dependency names event = ReliabilityEvent( component="api-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0, upstream_deps=["auth-service", "db-01", "cache-v2"] ) assert len(event.upstream_deps) == 3 # Invalid dependency names with pytest.raises(ValidationError): ReliabilityEvent( component="api-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0, upstream_deps=["AUTH_SERVICE"] # Uppercase/underscore ) class TestPolicyConditionModel: """Test PolicyCondition structured model""" def test_valid_policy_condition(self): """Test creating valid policy conditions""" condition = PolicyCondition( metric="latency_p99", operator="gt", threshold=150.0 ) assert condition.metric == "latency_p99" assert condition.operator == "gt" assert condition.threshold == 150.0 def test_policy_condition_frozen(self): """Test that PolicyCondition is immutable""" condition = PolicyCondition( metric="error_rate", operator="gt", threshold=0.1 ) with pytest.raises(ValidationError): condition.threshold = 0.2 def test_invalid_metric(self): """Test that invalid metrics are rejected""" with pytest.raises(ValidationError): PolicyCondition( metric="invalid_metric", operator="gt", threshold=100.0 ) def test_invalid_operator(self): """Test that invalid operators are rejected""" with pytest.raises(ValidationError): PolicyCondition( metric="latency_p99", operator="invalid_op", threshold=100.0 ) def test_negative_threshold(self): """Test that negative thresholds are rejected""" with pytest.raises(ValidationError): PolicyCondition( metric="latency_p99", operator="gt", threshold=-100.0 ) class TestHealingPolicyModel: """Test HealingPolicy model""" def test_valid_healing_policy(self): """Test creating valid healing policy""" policy = HealingPolicy( name="high_latency_restart", conditions=[ PolicyCondition(metric="latency_p99", operator="gt", threshold=300.0) ], actions=[HealingAction.RESTART_CONTAINER, HealingAction.ALERT_TEAM], priority=1, cool_down_seconds=300 ) assert policy.name == "high_latency_restart" assert len(policy.conditions) == 1 assert len(policy.actions) == 2 assert policy.priority == 1 def test_policy_frozen(self): """Test that HealingPolicy is immutable""" policy = HealingPolicy( name="test_policy", conditions=[ PolicyCondition(metric="error_rate", operator="gt", threshold=0.1) ], actions=[HealingAction.ROLLBACK], priority=2 ) with pytest.raises(ValidationError): policy.priority = 5 def test_empty_conditions_rejected(self): """Test that policies must have at least one condition""" with pytest.raises(ValidationError): HealingPolicy( name="empty_policy", conditions=[], # Empty actions=[HealingAction.ALERT_TEAM], priority=3 ) def test_empty_actions_rejected(self): """Test that policies must have at least one action""" with pytest.raises(ValidationError): HealingPolicy( name="empty_actions", conditions=[ PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0) ], actions=[], # Empty priority=3 ) def test_priority_bounds(self): """Test priority validation (1-5)""" # Valid priority policy = HealingPolicy( name="test", conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)], actions=[HealingAction.ALERT_TEAM], priority=3 ) assert policy.priority == 3 # Priority < 1 should fail with pytest.raises(ValidationError): HealingPolicy( name="test", conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)], actions=[HealingAction.ALERT_TEAM], priority=0 ) # Priority > 5 should fail with pytest.raises(ValidationError): HealingPolicy( name="test", conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)], actions=[HealingAction.ALERT_TEAM], priority=10 ) class TestAnomalyResultModel: """Test AnomalyResult model""" def test_valid_anomaly_result(self): """Test creating valid anomaly result""" result = AnomalyResult( is_anomaly=True, confidence=0.85, anomaly_score=0.75, affected_metrics=["latency", "error_rate"] ) assert result.is_anomaly is True assert result.confidence == 0.85 assert isinstance(result.detection_timestamp, datetime) def test_confidence_bounds(self): """Test confidence is bounded 0-1""" # Valid result = AnomalyResult( is_anomaly=True, confidence=0.5, anomaly_score=0.6 ) assert result.confidence == 0.5 # Confidence > 1 should fail with pytest.raises(ValidationError): AnomalyResult( is_anomaly=True, confidence=1.5, anomaly_score=0.5 ) class TestForecastResultModel: """Test ForecastResult model""" def test_valid_forecast(self): """Test creating valid forecast""" result = ForecastResult( metric="latency", predicted_value=250.0, confidence=0.75, trend="increasing", time_to_threshold=15.5, risk_level="high" ) assert result.metric == "latency" assert result.trend == "increasing" assert result.risk_level == "high" def test_trend_validation(self): """Test that only valid trends are accepted""" valid_trends = ["increasing", "decreasing", "stable"] for trend in valid_trends: result = ForecastResult( metric="latency", predicted_value=200.0, confidence=0.7, trend=trend, risk_level="medium" ) assert result.trend == trend # Invalid trend with pytest.raises(ValidationError): ForecastResult( metric="latency", predicted_value=200.0, confidence=0.7, trend="invalid_trend", risk_level="medium" ) def test_risk_level_validation(self): """Test that only valid risk levels are accepted""" valid_levels = ["low", "medium", "high", "critical"] for level in valid_levels: result = ForecastResult( metric="error_rate", predicted_value=0.08, confidence=0.8, trend="stable", risk_level=level ) assert result.risk_level == level # Invalid risk level with pytest.raises(ValidationError): ForecastResult( metric="error_rate", predicted_value=0.08, confidence=0.8, trend="stable", risk_level="extreme" ) class TestTimestampHandling: """Test datetime timestamp handling""" def test_timestamp_is_datetime(self): """Test that timestamp is datetime, not string""" event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) # Should be datetime object assert isinstance(event.timestamp, datetime) # Should have timezone assert event.timestamp.tzinfo is not None def test_timestamp_is_utc(self): """Test that timestamp uses UTC""" event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) assert event.timestamp.tzinfo == timezone.utc def test_timestamp_serialization(self): """Test that timestamp can be serialized""" event = ReliabilityEvent( component="test-service", latency_p99=100.0, error_rate=0.05, throughput=1000.0 ) # Can convert to ISO format iso_str = event.timestamp.isoformat() assert isinstance(iso_str, str) assert 'T' in iso_str # ISO format if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])