190 lines
5.6 KiB
PHP
190 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Helpers\SshRetryHandler;
|
|
use App\Traits\SshRetryable;
|
|
use Tests\TestCase;
|
|
|
|
class SshRetryMechanismTest extends TestCase
|
|
{
|
|
public function test_ssh_retry_handler_exists()
|
|
{
|
|
$this->assertTrue(class_exists(\App\Helpers\SshRetryHandler::class));
|
|
}
|
|
|
|
public function test_ssh_retryable_trait_exists()
|
|
{
|
|
$this->assertTrue(trait_exists(\App\Traits\SshRetryable::class));
|
|
}
|
|
|
|
public function test_retry_on_ssh_connection_errors()
|
|
{
|
|
$handler = new class
|
|
{
|
|
use SshRetryable;
|
|
|
|
// Make methods public for testing
|
|
public function test_is_retryable_ssh_error($error)
|
|
{
|
|
return $this->isRetryableSshError($error);
|
|
}
|
|
};
|
|
|
|
// Test various SSH error patterns
|
|
$sshErrors = [
|
|
'kex_exchange_identification: read: Connection reset by peer',
|
|
'Connection refused',
|
|
'Connection timed out',
|
|
'ssh_exchange_identification: Connection closed by remote host',
|
|
'Broken pipe',
|
|
'No route to host',
|
|
'Network is unreachable',
|
|
];
|
|
|
|
foreach ($sshErrors as $error) {
|
|
$this->assertTrue(
|
|
$handler->test_is_retryable_ssh_error($error),
|
|
"Failed to identify as retryable: $error"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function test_non_ssh_errors_are_not_retryable()
|
|
{
|
|
$handler = new class
|
|
{
|
|
use SshRetryable;
|
|
|
|
// Make methods public for testing
|
|
public function test_is_retryable_ssh_error($error)
|
|
{
|
|
return $this->isRetryableSshError($error);
|
|
}
|
|
};
|
|
|
|
// Test non-SSH errors
|
|
$nonSshErrors = [
|
|
'Command not found',
|
|
'Permission denied',
|
|
'File not found',
|
|
'Syntax error',
|
|
'Invalid argument',
|
|
];
|
|
|
|
foreach ($nonSshErrors as $error) {
|
|
$this->assertFalse(
|
|
$handler->test_is_retryable_ssh_error($error),
|
|
"Incorrectly identified as retryable: $error"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function test_exponential_backoff_calculation()
|
|
{
|
|
$handler = new class
|
|
{
|
|
use SshRetryable;
|
|
|
|
// Make method public for testing
|
|
public function test_calculate_retry_delay($attempt)
|
|
{
|
|
return $this->calculateRetryDelay($attempt);
|
|
}
|
|
};
|
|
|
|
// Test with default config values
|
|
config(['constants.ssh.retry_base_delay' => 2]);
|
|
config(['constants.ssh.retry_max_delay' => 30]);
|
|
config(['constants.ssh.retry_multiplier' => 2]);
|
|
|
|
// Attempt 0: 2 seconds
|
|
$this->assertEquals(2, $handler->test_calculate_retry_delay(0));
|
|
|
|
// Attempt 1: 4 seconds
|
|
$this->assertEquals(4, $handler->test_calculate_retry_delay(1));
|
|
|
|
// Attempt 2: 8 seconds
|
|
$this->assertEquals(8, $handler->test_calculate_retry_delay(2));
|
|
|
|
// Attempt 3: 16 seconds
|
|
$this->assertEquals(16, $handler->test_calculate_retry_delay(3));
|
|
|
|
// Attempt 4: Should be capped at 30 seconds
|
|
$this->assertEquals(30, $handler->test_calculate_retry_delay(4));
|
|
|
|
// Attempt 5: Should still be capped at 30 seconds
|
|
$this->assertEquals(30, $handler->test_calculate_retry_delay(5));
|
|
}
|
|
|
|
public function test_retry_succeeds_after_failures()
|
|
{
|
|
$attemptCount = 0;
|
|
|
|
config(['constants.ssh.max_retries' => 3]);
|
|
|
|
// Simulate a function that fails twice then succeeds using the public static method
|
|
$result = SshRetryHandler::retry(
|
|
function () use (&$attemptCount) {
|
|
$attemptCount++;
|
|
if ($attemptCount < 3) {
|
|
throw new \RuntimeException('kex_exchange_identification: Connection reset by peer');
|
|
}
|
|
|
|
return 'success';
|
|
},
|
|
['test' => 'retry_test'],
|
|
true
|
|
);
|
|
|
|
$this->assertEquals('success', $result);
|
|
$this->assertEquals(3, $attemptCount);
|
|
}
|
|
|
|
public function test_retry_fails_after_max_attempts()
|
|
{
|
|
$attemptCount = 0;
|
|
|
|
config(['constants.ssh.max_retries' => 3]);
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->expectExceptionMessage('Connection reset by peer');
|
|
|
|
// Simulate a function that always fails using the public static method
|
|
SshRetryHandler::retry(
|
|
function () use (&$attemptCount) {
|
|
$attemptCount++;
|
|
throw new \RuntimeException('Connection reset by peer');
|
|
},
|
|
['test' => 'retry_test'],
|
|
true
|
|
);
|
|
}
|
|
|
|
public function test_non_retryable_errors_fail_immediately()
|
|
{
|
|
$attemptCount = 0;
|
|
|
|
config(['constants.ssh.max_retries' => 3]);
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->expectExceptionMessage('Command not found');
|
|
|
|
try {
|
|
// Simulate a non-retryable error using the public static method
|
|
SshRetryHandler::retry(
|
|
function () use (&$attemptCount) {
|
|
$attemptCount++;
|
|
throw new \RuntimeException('Command not found');
|
|
},
|
|
['test' => 'non_retryable_test'],
|
|
true
|
|
);
|
|
} catch (\RuntimeException $e) {
|
|
// Should only attempt once since it's not retryable
|
|
$this->assertEquals(1, $attemptCount);
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|