- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
288 lines
11 KiB
PHP
288 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\CircuitBreaker\CircuitBreakerInterface;
|
|
use App\Framework\CircuitBreaker\CircuitBreakerMetrics;
|
|
use App\Framework\CircuitBreaker\CircuitState;
|
|
use App\Framework\DDoS\Components\ServiceHealthAnalyzer;
|
|
use App\Framework\Logging\DefaultLogger;
|
|
use App\Framework\Logging\LogLevel;
|
|
|
|
require_once __DIR__ . '/../Helpers/TestHelpers.php';
|
|
|
|
beforeEach(function () {
|
|
// Create a mock that mimics CircuitBreaker behavior
|
|
$this->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);
|
|
});
|
|
|
|
});
|