mockMetricsData = []; $this->circuitBreaker = new class ($this->mockMetricsData) implements CircuitBreakerInterface { private array $metricsData; public function __construct(array &$metricsData) { $this->metricsData = &$metricsData; } public function getMetrics(string $service): CircuitBreakerMetrics { $data = $this->metricsData[$service] ?? [ 'failure_count' => 0, 'success_count' => 0, 'state' => CircuitState::CLOSED, ]; return new CircuitBreakerMetrics( state: $data['state'], failureCount: $data['failure_count'], successCount: $data['success_count'], halfOpenAttempts: 0, lastFailureTime: null, openedAt: null ); } public function setTestMetrics(string $service, array $metrics): void { $this->metricsData[$service] = [ 'failure_count' => $metrics['failure_count'] ?? 0, 'success_count' => $metrics['success_count'] ?? 0, 'state' => $metrics['state'] ?? CircuitState::CLOSED, ]; } }; // Helper function to set metrics $this->setMetrics = function (string $service, array $metrics) { $this->circuitBreaker->setTestMetrics($service, $metrics); }; // Create logger $this->logger = new DefaultLogger(LogLevel::DEBUG); $this->analyzer = new ServiceHealthAnalyzer($this->circuitBreaker, $this->logger); }); describe('ServiceHealthAnalyzer', function () { it('analyzes service health for normal conditions', function () { $clientIp = '192.168.1.100'; // Set up healthy services ($this->setMetrics)('web', ['failure_count' => 1, 'success_count' => 99]); ($this->setMetrics)('api', ['failure_count' => 0, 'success_count' => 100]); ($this->setMetrics)('database', ['failure_count' => 2, 'success_count' => 98]); $health = $this->analyzer->analyzeServiceHealth($clientIp); expect($health)->toHaveKeys([ 'overall_health', 'service_scores', 'degraded_services', 'confidence', 'threat_score', ]); expect($health['threat_score'])->toBeBetween(0.0, 1.0); expect($health['confidence'])->toBe(0.9); expect($health['overall_health'])->toBeLessThan(0.1); // Low threat score means healthy }); it('detects degraded services', function () { $clientIp = '192.168.1.200'; // Set up degraded services with high failure rates ($this->setMetrics)('web', ['failure_count' => 40, 'success_count' => 60]); ($this->setMetrics)('api', ['failure_count' => 45, 'success_count' => 55]); ($this->setMetrics)('database', ['failure_count' => 2, 'success_count' => 98]); $health = $this->analyzer->analyzeServiceHealth($clientIp); expect($health['overall_health'])->toBeGreaterThan(0.5); // Higher threat score expect($health['degraded_services'])->not()->toBeEmpty(); expect($health['service_scores']['web'])->toBeGreaterThan(0.5); expect($health['service_scores']['api'])->toBeGreaterThan(0.5); }); it('handles completely failed services', function () { $clientIp = '192.168.1.300'; // Set up failed services with very high failure rates ($this->setMetrics)('web', ['failure_count' => 50, 'success_count' => 0]); ($this->setMetrics)('api', ['failure_count' => 40, 'success_count' => 10]); ($this->setMetrics)('database', ['failure_count' => 48, 'success_count' => 2]); $health = $this->analyzer->analyzeServiceHealth($clientIp); expect($health['overall_health'])->toBeGreaterThan(0.8); // Very high threat score expect($health['degraded_services'])->toHaveCount(3); // All services degraded expect($health['service_scores']['web'])->toBe(1.0); // Maximum degradation (100% failure rate) }); it('handles missing metrics gracefully', function () { $clientIp = '192.168.1.400'; // Don't set up any metrics, analyzer should use default empty metrics $health = $this->analyzer->analyzeServiceHealth($clientIp); expect($health)->toHaveKeys([ 'overall_health', 'service_scores', 'degraded_services', 'confidence', 'threat_score', ]); expect($health['overall_health'])->toBeBetween(0.0, 1.0); expect($health['confidence'])->toBe(0.9); }); it('calculates health scores correctly from failure rates', function () { $clientIp = '192.168.1.500'; // Test different failure rate scenarios with custom services ($this->setMetrics)('service1', ['failure_count' => 10, 'success_count' => 90]); ($this->setMetrics)('service2', ['failure_count' => 25, 'success_count' => 25]); $analyzer = new ServiceHealthAnalyzer( $this->circuitBreaker, $this->logger, ['service1', 'service2'] ); $health = $analyzer->analyzeServiceHealth($clientIp); // service1: 10/(10+90) = 0.1 failure rate -> 0.2 threat score expect($health['service_scores']['service1'])->toBe(0.2); // service2: 25/(25+25) = 0.5 failure rate -> 1.0 threat score (capped) expect($health['service_scores']['service2'])->toBe(1.0); // Overall: (0.2 + 1.0) / 2 = 0.6 expect($health['overall_health'])->toBe(0.6); }); it('identifies degraded services correctly', function () { $clientIp = '192.168.1.600'; ($this->setMetrics)('healthy_service', ['failure_count' => 5, 'success_count' => 95]); ($this->setMetrics)('degraded_service', ['failure_count' => 40, 'success_count' => 60]); $analyzer = new ServiceHealthAnalyzer( $this->circuitBreaker, $this->logger, ['healthy_service', 'degraded_service'] ); $health = $analyzer->analyzeServiceHealth($clientIp); // Only services with score > 0.7 are considered degraded expect($health['degraded_services'])->toHaveKey('degraded_service'); expect($health['degraded_services'])->not()->toHaveKey('healthy_service'); expect($health['degraded_services']['degraded_service'])->toBe(0.8); }); it('uses custom service list when provided', function () { $clientIp = '192.168.1.800'; $customServices = ['redis', 'elasticsearch', 'rabbitmq']; ($this->setMetrics)('redis', ['failure_count' => 2, 'success_count' => 98]); ($this->setMetrics)('elasticsearch', ['failure_count' => 15, 'success_count' => 85]); ($this->setMetrics)('rabbitmq', ['failure_count' => 8, 'success_count' => 92]); $analyzer = new ServiceHealthAnalyzer( $this->circuitBreaker, $this->logger, $customServices ); $health = $analyzer->analyzeServiceHealth($clientIp); expect($health['service_scores'])->toHaveKeys($customServices); expect($health['service_scores'])->not()->toHaveKey('web'); // Default service not included expect($health['service_scores'])->not()->toHaveKey('api'); // Default service not included expect($health['service_scores'])->not()->toHaveKey('database'); // Default service not included }); it('handles circuit breaker exceptions gracefully', function () { $clientIp = '192.168.1.700'; // Create a CircuitBreaker that throws exceptions $faultyCircuitBreaker = new class () implements CircuitBreakerInterface { public function getMetrics(string $service): CircuitBreakerMetrics { throw new \RuntimeException("Circuit breaker service unavailable"); } }; $analyzer = new ServiceHealthAnalyzer($faultyCircuitBreaker, $this->logger); $health = $analyzer->analyzeServiceHealth($clientIp); // Should handle exception gracefully and use neutral score (0.5) expect($health['service_scores']['web'])->toBe(0.5); expect($health['service_scores']['api'])->toBe(0.5); expect($health['service_scores']['database'])->toBe(0.5); expect($health['overall_health'])->toBe(0.5); }); }); describe('Health Score Calculations', function () { it('calculates correct health scores from different failure rates', function () { $clientIp = '192.168.1.100'; $testCases = [ // [failures, successes, expected_score] [0, 100, 0.0], // 0% failure rate -> 0.0 * 2.0 = 0.0 [5, 95, 0.1], // 5% failure rate -> 0.05 * 2.0 = 0.1 [10, 90, 0.2], // 10% failure rate -> 0.1 * 2.0 = 0.2 [25, 75, 0.5], // 25% failure rate -> 0.25 * 2.0 = 0.5 [35, 65, 0.7], // 35% failure rate -> 0.35 * 2.0 = 0.7 [25, 25, 1.0], // 50% failure rate -> 0.5 * 2.0 = 1.0 (capped) ]; foreach ($testCases as [$failures, $successes, $expectedScore]) { ($this->setMetrics)('test_service', [ 'failure_count' => $failures, 'success_count' => $successes, ]); $analyzer = new ServiceHealthAnalyzer( $this->circuitBreaker, $this->logger, ['test_service'] ); $health = $analyzer->analyzeServiceHealth($clientIp); $actualScore = $health['service_scores']['test_service']; $failureRate = $failures / max(1, $failures + $successes); $calculatedScore = min(1.0, $failureRate * 2.0); expect($actualScore)->toBe( $calculatedScore, "Expected score for {$failures} failures, {$successes} successes: {$calculatedScore}, got: {$actualScore}" ); } }); it('handles edge cases in failure rate calculation', function () { $clientIp = '192.168.1.200'; // Test case: 0 total requests (fresh service) $analyzer = new ServiceHealthAnalyzer( $this->circuitBreaker, $this->logger, ['empty_service'] ); $health = $analyzer->analyzeServiceHealth($clientIp); // Should handle division by zero gracefully - fresh service has 0 failures, 0 successes // max(1, 0+0) = 1, so 0/1 = 0.0 failure rate expect($health['service_scores']['empty_service'])->toBe(0.0); }); });