petter2025 commited on
Commit
892c7af
·
verified ·
1 Parent(s): b211251

Create test_models.py

Browse files
Files changed (1) hide show
  1. tests/test_models.py +614 -0
tests/test_models.py ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unit tests for Pydantic models with validation and security tests
3
+ """
4
+
5
+ import pytest
6
+ from datetime import datetime, timezone
7
+ from pydantic import ValidationError
8
+ from models import (
9
+ ReliabilityEvent,
10
+ EventSeverity,
11
+ HealingPolicy,
12
+ HealingAction,
13
+ PolicyCondition,
14
+ AnomalyResult,
15
+ ForecastResult
16
+ )
17
+
18
+
19
+ class TestReliabilityEventValidation:
20
+ """Test ReliabilityEvent validation"""
21
+
22
+ def test_valid_event_creation(self):
23
+ """Test creating a valid event"""
24
+ event = ReliabilityEvent(
25
+ component="api-service",
26
+ latency_p99=150.0,
27
+ error_rate=0.05,
28
+ throughput=1000.0,
29
+ cpu_util=0.7,
30
+ memory_util=0.6
31
+ )
32
+
33
+ assert event.component == "api-service"
34
+ assert event.latency_p99 == 150.0
35
+ assert event.error_rate == 0.05
36
+ assert isinstance(event.timestamp, datetime)
37
+ assert event.severity == EventSeverity.LOW
38
+
39
+ def test_component_validation_valid(self):
40
+ """Test valid component IDs"""
41
+ valid_ids = ["api-service", "auth-service", "payment-service-v2", "db-01"]
42
+
43
+ for component_id in valid_ids:
44
+ event = ReliabilityEvent(
45
+ component=component_id,
46
+ latency_p99=100.0,
47
+ error_rate=0.01,
48
+ throughput=1000.0
49
+ )
50
+ assert event.component == component_id
51
+
52
+ def test_component_validation_invalid(self):
53
+ """Test invalid component IDs are rejected"""
54
+ invalid_ids = [
55
+ "API-SERVICE", # Uppercase
56
+ "api_service", # Underscore
57
+ "api service", # Space
58
+ "api@service", # Special char
59
+ "", # Empty
60
+ ]
61
+
62
+ for component_id in invalid_ids:
63
+ with pytest.raises(ValidationError) as exc_info:
64
+ ReliabilityEvent(
65
+ component=component_id,
66
+ latency_p99=100.0,
67
+ error_rate=0.01,
68
+ throughput=1000.0
69
+ )
70
+ assert "component" in str(exc_info.value).lower()
71
+
72
+ def test_latency_bounds(self):
73
+ """Test latency validation bounds"""
74
+ # Valid latency
75
+ event = ReliabilityEvent(
76
+ component="test-service",
77
+ latency_p99=100.0,
78
+ error_rate=0.01,
79
+ throughput=1000.0
80
+ )
81
+ assert event.latency_p99 == 100.0
82
+
83
+ # Negative latency should fail
84
+ with pytest.raises(ValidationError):
85
+ ReliabilityEvent(
86
+ component="test-service",
87
+ latency_p99=-10.0,
88
+ error_rate=0.01,
89
+ throughput=1000.0
90
+ )
91
+
92
+ # Extremely high latency should fail (> 5 minutes)
93
+ with pytest.raises(ValidationError):
94
+ ReliabilityEvent(
95
+ component="test-service",
96
+ latency_p99=400000.0, # > 300000ms limit
97
+ error_rate=0.01,
98
+ throughput=1000.0
99
+ )
100
+
101
+ def test_error_rate_bounds(self):
102
+ """Test error rate validation"""
103
+ # Valid error rate
104
+ event = ReliabilityEvent(
105
+ component="test-service",
106
+ latency_p99=100.0,
107
+ error_rate=0.5,
108
+ throughput=1000.0
109
+ )
110
+ assert event.error_rate == 0.5
111
+
112
+ # Negative error rate should fail
113
+ with pytest.raises(ValidationError):
114
+ ReliabilityEvent(
115
+ component="test-service",
116
+ latency_p99=100.0,
117
+ error_rate=-0.1,
118
+ throughput=1000.0
119
+ )
120
+
121
+ # Error rate > 1 should fail
122
+ with pytest.raises(ValidationError):
123
+ ReliabilityEvent(
124
+ component="test-service",
125
+ latency_p99=100.0,
126
+ error_rate=1.5,
127
+ throughput=1000.0
128
+ )
129
+
130
+ def test_resource_utilization_bounds(self):
131
+ """Test CPU and memory utilization bounds"""
132
+ # Valid utilization
133
+ event = ReliabilityEvent(
134
+ component="test-service",
135
+ latency_p99=100.0,
136
+ error_rate=0.01,
137
+ throughput=1000.0,
138
+ cpu_util=0.85,
139
+ memory_util=0.75
140
+ )
141
+ assert event.cpu_util == 0.85
142
+ assert event.memory_util == 0.75
143
+
144
+ # CPU > 1 should fail
145
+ with pytest.raises(ValidationError):
146
+ ReliabilityEvent(
147
+ component="test-service",
148
+ latency_p99=100.0,
149
+ error_rate=0.01,
150
+ throughput=1000.0,
151
+ cpu_util=1.5
152
+ )
153
+
154
+ # Memory < 0 should fail
155
+ with pytest.raises(ValidationError):
156
+ ReliabilityEvent(
157
+ component="test-service",
158
+ latency_p99=100.0,
159
+ error_rate=0.01,
160
+ throughput=1000.0,
161
+ memory_util=-0.1
162
+ )
163
+
164
+
165
+ class TestEventFingerprint:
166
+ """Test event fingerprint generation (SHA-256)"""
167
+
168
+ def test_fingerprint_is_sha256(self):
169
+ """Test that fingerprint uses SHA-256 (64 hex chars)"""
170
+ event = ReliabilityEvent(
171
+ component="test-service",
172
+ latency_p99=100.0,
173
+ error_rate=0.05,
174
+ throughput=1000.0
175
+ )
176
+
177
+ # SHA-256 produces 64 hex characters
178
+ assert len(event.fingerprint) == 64
179
+ assert all(c in '0123456789abcdef' for c in event.fingerprint)
180
+
181
+ def test_fingerprint_deterministic(self):
182
+ """Test that same inputs produce same fingerprint"""
183
+ event1 = ReliabilityEvent(
184
+ component="test-service",
185
+ service_mesh="default",
186
+ latency_p99=100.0,
187
+ error_rate=0.05,
188
+ throughput=1000.0
189
+ )
190
+
191
+ event2 = ReliabilityEvent(
192
+ component="test-service",
193
+ service_mesh="default",
194
+ latency_p99=100.0,
195
+ error_rate=0.05,
196
+ throughput=1000.0
197
+ )
198
+
199
+ # Should produce same fingerprint (timestamp not included)
200
+ assert event1.fingerprint == event2.fingerprint
201
+
202
+ def test_fingerprint_different_for_different_events(self):
203
+ """Test that different events produce different fingerprints"""
204
+ event1 = ReliabilityEvent(
205
+ component="service-1",
206
+ latency_p99=100.0,
207
+ error_rate=0.05,
208
+ throughput=1000.0
209
+ )
210
+
211
+ event2 = ReliabilityEvent(
212
+ component="service-2",
213
+ latency_p99=100.0,
214
+ error_rate=0.05,
215
+ throughput=1000.0
216
+ )
217
+
218
+ assert event1.fingerprint != event2.fingerprint
219
+
220
+ def test_fingerprint_not_md5(self):
221
+ """Test that fingerprint is NOT MD5 (security fix verification)"""
222
+ event = ReliabilityEvent(
223
+ component="test-service",
224
+ latency_p99=100.0,
225
+ error_rate=0.05,
226
+ throughput=1000.0
227
+ )
228
+
229
+ # MD5 produces 32 hex chars, SHA-256 produces 64
230
+ assert len(event.fingerprint) != 32
231
+ assert len(event.fingerprint) == 64
232
+
233
+
234
+ class TestEventImmutability:
235
+ """Test that events are immutable (frozen)"""
236
+
237
+ def test_event_is_frozen(self):
238
+ """Test that ReliabilityEvent is frozen"""
239
+ event = ReliabilityEvent(
240
+ component="test-service",
241
+ latency_p99=100.0,
242
+ error_rate=0.05,
243
+ throughput=1000.0
244
+ )
245
+
246
+ # Attempting to modify should raise ValidationError
247
+ with pytest.raises(ValidationError):
248
+ event.latency_p99 = 200.0
249
+
250
+ def test_model_copy_with_update(self):
251
+ """Test that model_copy creates new instance with updates"""
252
+ event1 = ReliabilityEvent(
253
+ component="test-service",
254
+ latency_p99=100.0,
255
+ error_rate=0.05,
256
+ throughput=1000.0,
257
+ severity=EventSeverity.LOW
258
+ )
259
+
260
+ # Create modified copy
261
+ event2 = event1.model_copy(update={'severity': EventSeverity.HIGH})
262
+
263
+ # Original unchanged
264
+ assert event1.severity == EventSeverity.LOW
265
+ # Copy updated
266
+ assert event2.severity == EventSeverity.HIGH
267
+ # Other fields same
268
+ assert event2.component == event1.component
269
+ assert event2.latency_p99 == event1.latency_p99
270
+
271
+
272
+ class TestDependencyValidation:
273
+ """Test dependency cycle detection"""
274
+
275
+ def test_valid_dependencies(self):
276
+ """Test valid dependency configuration"""
277
+ event = ReliabilityEvent(
278
+ component="api-service",
279
+ latency_p99=100.0,
280
+ error_rate=0.05,
281
+ throughput=1000.0,
282
+ upstream_deps=["auth-service", "database"],
283
+ downstream_deps=["frontend", "mobile-app"]
284
+ )
285
+
286
+ assert "auth-service" in event.upstream_deps
287
+ assert "frontend" in event.downstream_deps
288
+
289
+ def test_circular_dependency_detected(self):
290
+ """Test that circular dependencies are detected"""
291
+ with pytest.raises(ValidationError) as exc_info:
292
+ ReliabilityEvent(
293
+ component="api-service",
294
+ latency_p99=100.0,
295
+ error_rate=0.05,
296
+ throughput=1000.0,
297
+ upstream_deps=["auth-service", "database"],
298
+ downstream_deps=["database", "frontend"] # 'database' in both
299
+ )
300
+
301
+ error_msg = str(exc_info.value).lower()
302
+ assert "circular" in error_msg or "database" in error_msg
303
+
304
+ def test_dependency_name_validation(self):
305
+ """Test that dependency names follow same rules as component IDs"""
306
+ # Valid dependency names
307
+ event = ReliabilityEvent(
308
+ component="api-service",
309
+ latency_p99=100.0,
310
+ error_rate=0.05,
311
+ throughput=1000.0,
312
+ upstream_deps=["auth-service", "db-01", "cache-v2"]
313
+ )
314
+ assert len(event.upstream_deps) == 3
315
+
316
+ # Invalid dependency names
317
+ with pytest.raises(ValidationError):
318
+ ReliabilityEvent(
319
+ component="api-service",
320
+ latency_p99=100.0,
321
+ error_rate=0.05,
322
+ throughput=1000.0,
323
+ upstream_deps=["AUTH_SERVICE"] # Uppercase/underscore
324
+ )
325
+
326
+
327
+ class TestPolicyConditionModel:
328
+ """Test PolicyCondition structured model"""
329
+
330
+ def test_valid_policy_condition(self):
331
+ """Test creating valid policy conditions"""
332
+ condition = PolicyCondition(
333
+ metric="latency_p99",
334
+ operator="gt",
335
+ threshold=150.0
336
+ )
337
+
338
+ assert condition.metric == "latency_p99"
339
+ assert condition.operator == "gt"
340
+ assert condition.threshold == 150.0
341
+
342
+ def test_policy_condition_frozen(self):
343
+ """Test that PolicyCondition is immutable"""
344
+ condition = PolicyCondition(
345
+ metric="error_rate",
346
+ operator="gt",
347
+ threshold=0.1
348
+ )
349
+
350
+ with pytest.raises(ValidationError):
351
+ condition.threshold = 0.2
352
+
353
+ def test_invalid_metric(self):
354
+ """Test that invalid metrics are rejected"""
355
+ with pytest.raises(ValidationError):
356
+ PolicyCondition(
357
+ metric="invalid_metric",
358
+ operator="gt",
359
+ threshold=100.0
360
+ )
361
+
362
+ def test_invalid_operator(self):
363
+ """Test that invalid operators are rejected"""
364
+ with pytest.raises(ValidationError):
365
+ PolicyCondition(
366
+ metric="latency_p99",
367
+ operator="invalid_op",
368
+ threshold=100.0
369
+ )
370
+
371
+ def test_negative_threshold(self):
372
+ """Test that negative thresholds are rejected"""
373
+ with pytest.raises(ValidationError):
374
+ PolicyCondition(
375
+ metric="latency_p99",
376
+ operator="gt",
377
+ threshold=-100.0
378
+ )
379
+
380
+
381
+ class TestHealingPolicyModel:
382
+ """Test HealingPolicy model"""
383
+
384
+ def test_valid_healing_policy(self):
385
+ """Test creating valid healing policy"""
386
+ policy = HealingPolicy(
387
+ name="high_latency_restart",
388
+ conditions=[
389
+ PolicyCondition(metric="latency_p99", operator="gt", threshold=300.0)
390
+ ],
391
+ actions=[HealingAction.RESTART_CONTAINER, HealingAction.ALERT_TEAM],
392
+ priority=1,
393
+ cool_down_seconds=300
394
+ )
395
+
396
+ assert policy.name == "high_latency_restart"
397
+ assert len(policy.conditions) == 1
398
+ assert len(policy.actions) == 2
399
+ assert policy.priority == 1
400
+
401
+ def test_policy_frozen(self):
402
+ """Test that HealingPolicy is immutable"""
403
+ policy = HealingPolicy(
404
+ name="test_policy",
405
+ conditions=[
406
+ PolicyCondition(metric="error_rate", operator="gt", threshold=0.1)
407
+ ],
408
+ actions=[HealingAction.ROLLBACK],
409
+ priority=2
410
+ )
411
+
412
+ with pytest.raises(ValidationError):
413
+ policy.priority = 5
414
+
415
+ def test_empty_conditions_rejected(self):
416
+ """Test that policies must have at least one condition"""
417
+ with pytest.raises(ValidationError):
418
+ HealingPolicy(
419
+ name="empty_policy",
420
+ conditions=[], # Empty
421
+ actions=[HealingAction.ALERT_TEAM],
422
+ priority=3
423
+ )
424
+
425
+ def test_empty_actions_rejected(self):
426
+ """Test that policies must have at least one action"""
427
+ with pytest.raises(ValidationError):
428
+ HealingPolicy(
429
+ name="empty_actions",
430
+ conditions=[
431
+ PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)
432
+ ],
433
+ actions=[], # Empty
434
+ priority=3
435
+ )
436
+
437
+ def test_priority_bounds(self):
438
+ """Test priority validation (1-5)"""
439
+ # Valid priority
440
+ policy = HealingPolicy(
441
+ name="test",
442
+ conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
443
+ actions=[HealingAction.ALERT_TEAM],
444
+ priority=3
445
+ )
446
+ assert policy.priority == 3
447
+
448
+ # Priority < 1 should fail
449
+ with pytest.raises(ValidationError):
450
+ HealingPolicy(
451
+ name="test",
452
+ conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
453
+ actions=[HealingAction.ALERT_TEAM],
454
+ priority=0
455
+ )
456
+
457
+ # Priority > 5 should fail
458
+ with pytest.raises(ValidationError):
459
+ HealingPolicy(
460
+ name="test",
461
+ conditions=[PolicyCondition(metric="latency_p99", operator="gt", threshold=100.0)],
462
+ actions=[HealingAction.ALERT_TEAM],
463
+ priority=10
464
+ )
465
+
466
+
467
+ class TestAnomalyResultModel:
468
+ """Test AnomalyResult model"""
469
+
470
+ def test_valid_anomaly_result(self):
471
+ """Test creating valid anomaly result"""
472
+ result = AnomalyResult(
473
+ is_anomaly=True,
474
+ confidence=0.85,
475
+ anomaly_score=0.75,
476
+ affected_metrics=["latency", "error_rate"]
477
+ )
478
+
479
+ assert result.is_anomaly is True
480
+ assert result.confidence == 0.85
481
+ assert isinstance(result.detection_timestamp, datetime)
482
+
483
+ def test_confidence_bounds(self):
484
+ """Test confidence is bounded 0-1"""
485
+ # Valid
486
+ result = AnomalyResult(
487
+ is_anomaly=True,
488
+ confidence=0.5,
489
+ anomaly_score=0.6
490
+ )
491
+ assert result.confidence == 0.5
492
+
493
+ # Confidence > 1 should fail
494
+ with pytest.raises(ValidationError):
495
+ AnomalyResult(
496
+ is_anomaly=True,
497
+ confidence=1.5,
498
+ anomaly_score=0.5
499
+ )
500
+
501
+
502
+ class TestForecastResultModel:
503
+ """Test ForecastResult model"""
504
+
505
+ def test_valid_forecast(self):
506
+ """Test creating valid forecast"""
507
+ result = ForecastResult(
508
+ metric="latency",
509
+ predicted_value=250.0,
510
+ confidence=0.75,
511
+ trend="increasing",
512
+ time_to_threshold=15.5,
513
+ risk_level="high"
514
+ )
515
+
516
+ assert result.metric == "latency"
517
+ assert result.trend == "increasing"
518
+ assert result.risk_level == "high"
519
+
520
+ def test_trend_validation(self):
521
+ """Test that only valid trends are accepted"""
522
+ valid_trends = ["increasing", "decreasing", "stable"]
523
+
524
+ for trend in valid_trends:
525
+ result = ForecastResult(
526
+ metric="latency",
527
+ predicted_value=200.0,
528
+ confidence=0.7,
529
+ trend=trend,
530
+ risk_level="medium"
531
+ )
532
+ assert result.trend == trend
533
+
534
+ # Invalid trend
535
+ with pytest.raises(ValidationError):
536
+ ForecastResult(
537
+ metric="latency",
538
+ predicted_value=200.0,
539
+ confidence=0.7,
540
+ trend="invalid_trend",
541
+ risk_level="medium"
542
+ )
543
+
544
+ def test_risk_level_validation(self):
545
+ """Test that only valid risk levels are accepted"""
546
+ valid_levels = ["low", "medium", "high", "critical"]
547
+
548
+ for level in valid_levels:
549
+ result = ForecastResult(
550
+ metric="error_rate",
551
+ predicted_value=0.08,
552
+ confidence=0.8,
553
+ trend="stable",
554
+ risk_level=level
555
+ )
556
+ assert result.risk_level == level
557
+
558
+ # Invalid risk level
559
+ with pytest.raises(ValidationError):
560
+ ForecastResult(
561
+ metric="error_rate",
562
+ predicted_value=0.08,
563
+ confidence=0.8,
564
+ trend="stable",
565
+ risk_level="extreme"
566
+ )
567
+
568
+
569
+ class TestTimestampHandling:
570
+ """Test datetime timestamp handling"""
571
+
572
+ def test_timestamp_is_datetime(self):
573
+ """Test that timestamp is datetime, not string"""
574
+ event = ReliabilityEvent(
575
+ component="test-service",
576
+ latency_p99=100.0,
577
+ error_rate=0.05,
578
+ throughput=1000.0
579
+ )
580
+
581
+ # Should be datetime object
582
+ assert isinstance(event.timestamp, datetime)
583
+
584
+ # Should have timezone
585
+ assert event.timestamp.tzinfo is not None
586
+
587
+ def test_timestamp_is_utc(self):
588
+ """Test that timestamp uses UTC"""
589
+ event = ReliabilityEvent(
590
+ component="test-service",
591
+ latency_p99=100.0,
592
+ error_rate=0.05,
593
+ throughput=1000.0
594
+ )
595
+
596
+ assert event.timestamp.tzinfo == timezone.utc
597
+
598
+ def test_timestamp_serialization(self):
599
+ """Test that timestamp can be serialized"""
600
+ event = ReliabilityEvent(
601
+ component="test-service",
602
+ latency_p99=100.0,
603
+ error_rate=0.05,
604
+ throughput=1000.0
605
+ )
606
+
607
+ # Can convert to ISO format
608
+ iso_str = event.timestamp.isoformat()
609
+ assert isinstance(iso_str, str)
610
+ assert 'T' in iso_str # ISO format
611
+
612
+
613
+ if __name__ == "__main__":
614
+ pytest.main([__file__, "-v", "--tb=short"])