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;
 | |
|         }
 | |
|     }
 | |
| }
 | 
