| """ |
| 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", |
| "api_service", |
| "api service", |
| "api@service", |
| "", |
| ] |
| |
| 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""" |
| |
| event = ReliabilityEvent( |
| component="test-service", |
| latency_p99=100.0, |
| error_rate=0.01, |
| throughput=1000.0 |
| ) |
| assert event.latency_p99 == 100.0 |
| |
| |
| with pytest.raises(ValidationError): |
| ReliabilityEvent( |
| component="test-service", |
| latency_p99=-10.0, |
| error_rate=0.01, |
| throughput=1000.0 |
| ) |
| |
| |
| with pytest.raises(ValidationError): |
| ReliabilityEvent( |
| component="test-service", |
| latency_p99=400000.0, |
| error_rate=0.01, |
| throughput=1000.0 |
| ) |
| |
| def test_error_rate_bounds(self): |
| """Test error rate validation""" |
| |
| event = ReliabilityEvent( |
| component="test-service", |
| latency_p99=100.0, |
| error_rate=0.5, |
| throughput=1000.0 |
| ) |
| assert event.error_rate == 0.5 |
| |
| |
| with pytest.raises(ValidationError): |
| ReliabilityEvent( |
| component="test-service", |
| latency_p99=100.0, |
| error_rate=-0.1, |
| throughput=1000.0 |
| ) |
| |
| |
| 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""" |
| |
| 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 |
| |
| |
| with pytest.raises(ValidationError): |
| ReliabilityEvent( |
| component="test-service", |
| latency_p99=100.0, |
| error_rate=0.01, |
| throughput=1000.0, |
| cpu_util=1.5 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| event2 = event1.model_copy(update={'severity': EventSeverity.HIGH}) |
| |
| |
| assert event1.severity == EventSeverity.LOW |
| |
| assert event2.severity == EventSeverity.HIGH |
| |
| 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"] |
| ) |
| |
| 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""" |
| |
| 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 |
| |
| |
| with pytest.raises(ValidationError): |
| ReliabilityEvent( |
| component="api-service", |
| latency_p99=100.0, |
| error_rate=0.05, |
| throughput=1000.0, |
| upstream_deps=["AUTH_SERVICE"] |
| ) |
|
|
|
|
| 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=[], |
| 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=[], |
| priority=3 |
| ) |
| |
| def test_priority_bounds(self): |
| """Test priority validation (1-5)""" |
| |
| policy = HealingPolicy( |
| name="test", |
| conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)], |
| actions=[HealingAction.ALERT_TEAM], |
| priority=3 |
| ) |
| assert policy.priority == 3 |
| |
| |
| with pytest.raises(ValidationError): |
| HealingPolicy( |
| name="test", |
| conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)], |
| actions=[HealingAction.ALERT_TEAM], |
| priority=0 |
| ) |
| |
| |
| 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""" |
| |
| result = AnomalyResult( |
| is_anomaly=True, |
| confidence=0.5, |
| anomaly_score=0.6 |
| ) |
| assert result.confidence == 0.5 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| ) |
| |
| |
| assert isinstance(event.timestamp, datetime) |
| |
| |
| 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 |
| ) |
| |
| |
| iso_str = event.timestamp.isoformat() |
| assert isinstance(iso_str, str) |
| assert 'T' in iso_str |
|
|
|
|
| if __name__ == "__main__": |
| pytest.main([__file__, "-v", "--tb=short"]) |