coolify/tests/Unit/ApplicationDeploymentErrorLoggingTest.php
Andras Bacsai 97550f4066 fix(deployment): eliminate duplicate error logging in deployment methods
Wraps rolling_update(), health_check(), stop_running_container(), and
start_by_compose_file() with try-catch to ensure comprehensive error logging
happens in one place. Removes duplicate logging from intermediate catch blocks
since the failed() method already provides full error details including stack trace
and chained exception information.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 10:52:09 +01:00

258 lines
9.6 KiB
PHP

<?php
use App\Exceptions\DeploymentException;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use Mockery;
/**
* Test to verify that deployment errors are properly logged with comprehensive details.
*
* This test suite verifies the fix for issue #7113 where deployments fail without
* clear error messages. The fix ensures that all deployment failures log:
* - The exception message
* - The exception type/class
* - The exception code (if present)
* - The file and line where the error occurred
* - Previous exception details (if chained)
* - Stack trace (first 5 lines)
*/
it('logs comprehensive error details when failed() is called', function () {
// Create a mock exception with all properties
$innerException = new \RuntimeException('Connection refused', 111);
$exception = new DeploymentException(
'Failed to start container',
500,
$innerException
);
// Mock the application deployment queue
$mockQueue = Mockery::mock(ApplicationDeploymentQueue::class);
$logEntries = [];
// Capture all log entries
$mockQueue->shouldReceive('addLogEntry')
->withArgs(function ($message, $type = 'stdout') use (&$logEntries) {
$logEntries[] = ['message' => $message, 'type' => $type];
return true;
})
->atLeast()->once();
$mockQueue->shouldReceive('update')->andReturn(true);
// Mock Application and its relationships
$mockApplication = Mockery::mock(Application::class);
$mockApplication->shouldReceive('getAttribute')
->with('build_pack')
->andReturn('dockerfile');
$mockApplication->build_pack = 'dockerfile';
$mockSettings = Mockery::mock();
$mockSettings->shouldReceive('getAttribute')
->with('is_consistent_container_name_enabled')
->andReturn(false);
$mockSettings->shouldReceive('getAttribute')
->with('custom_internal_name')
->andReturn('');
$mockSettings->is_consistent_container_name_enabled = false;
$mockSettings->custom_internal_name = '';
$mockApplication->shouldReceive('getAttribute')
->with('settings')
->andReturn($mockSettings);
// Use reflection to set private properties and call the failed() method
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldAllowMockingProtectedMethods();
$reflection = new \ReflectionClass($job);
$queueProperty = $reflection->getProperty('application_deployment_queue');
$queueProperty->setAccessible(true);
$queueProperty->setValue($job, $mockQueue);
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$applicationProperty->setValue($job, $mockApplication);
$pullRequestProperty = $reflection->getProperty('pull_request_id');
$pullRequestProperty->setAccessible(true);
$pullRequestProperty->setValue($job, 0);
// Mock the failDeployment method to prevent errors
$job->shouldReceive('failDeployment')->andReturn();
$job->shouldReceive('execute_remote_command')->andReturn();
// Call the failed method
$failedMethod = $reflection->getMethod('failed');
$failedMethod->setAccessible(true);
$failedMethod->invoke($job, $exception);
// Verify comprehensive error logging
$errorMessages = array_column($logEntries, 'message');
$errorMessageString = implode("\n", $errorMessages);
// Check that all critical information is logged
expect($errorMessageString)->toContain('Deployment failed: Failed to start container');
expect($errorMessageString)->toContain('Error type: App\Exceptions\DeploymentException');
expect($errorMessageString)->toContain('Error code: 500');
expect($errorMessageString)->toContain('Location:');
expect($errorMessageString)->toContain('Caused by:');
expect($errorMessageString)->toContain('RuntimeException: Connection refused');
expect($errorMessageString)->toContain('Stack trace');
// Verify stderr type is used for error logging
$stderrEntries = array_filter($logEntries, fn ($entry) => $entry['type'] === 'stderr');
expect(count($stderrEntries))->toBeGreaterThan(0);
});
it('handles exceptions with no message gracefully', function () {
$exception = new \Exception;
$mockQueue = Mockery::mock(ApplicationDeploymentQueue::class);
$logEntries = [];
$mockQueue->shouldReceive('addLogEntry')
->withArgs(function ($message, $type = 'stdout') use (&$logEntries) {
$logEntries[] = ['message' => $message, 'type' => $type];
return true;
})
->atLeast()->once();
$mockQueue->shouldReceive('update')->andReturn(true);
$mockApplication = Mockery::mock(Application::class);
$mockApplication->shouldReceive('getAttribute')
->with('build_pack')
->andReturn('dockerfile');
$mockApplication->build_pack = 'dockerfile';
$mockSettings = Mockery::mock();
$mockSettings->shouldReceive('getAttribute')
->with('is_consistent_container_name_enabled')
->andReturn(false);
$mockSettings->shouldReceive('getAttribute')
->with('custom_internal_name')
->andReturn('');
$mockSettings->is_consistent_container_name_enabled = false;
$mockSettings->custom_internal_name = '';
$mockApplication->shouldReceive('getAttribute')
->with('settings')
->andReturn($mockSettings);
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldAllowMockingProtectedMethods();
$reflection = new \ReflectionClass($job);
$queueProperty = $reflection->getProperty('application_deployment_queue');
$queueProperty->setAccessible(true);
$queueProperty->setValue($job, $mockQueue);
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$applicationProperty->setValue($job, $mockApplication);
$pullRequestProperty = $reflection->getProperty('pull_request_id');
$pullRequestProperty->setAccessible(true);
$pullRequestProperty->setValue($job, 0);
$job->shouldReceive('failDeployment')->andReturn();
$job->shouldReceive('execute_remote_command')->andReturn();
$failedMethod = $reflection->getMethod('failed');
$failedMethod->setAccessible(true);
$failedMethod->invoke($job, $exception);
$errorMessages = array_column($logEntries, 'message');
$errorMessageString = implode("\n", $errorMessages);
// Should log "Unknown error occurred" for empty messages
expect($errorMessageString)->toContain('Unknown error occurred');
expect($errorMessageString)->toContain('Error type:');
});
it('wraps exceptions in deployment methods with DeploymentException', function () {
// Verify that our deployment methods wrap exceptions properly
$originalException = new \RuntimeException('Container not found');
try {
throw new DeploymentException('Failed to start container', 0, $originalException);
} catch (DeploymentException $e) {
expect($e->getMessage())->toBe('Failed to start container');
expect($e->getPrevious())->toBe($originalException);
expect($e->getPrevious()->getMessage())->toBe('Container not found');
}
});
it('logs error code 0 correctly', function () {
// Verify that error code 0 is logged (previously skipped due to falsy check)
$exception = new \Exception('Test error', 0);
$mockQueue = Mockery::mock(ApplicationDeploymentQueue::class);
$logEntries = [];
$mockQueue->shouldReceive('addLogEntry')
->withArgs(function ($message, $type = 'stdout') use (&$logEntries) {
$logEntries[] = ['message' => $message, 'type' => $type];
return true;
})
->atLeast()->once();
$mockQueue->shouldReceive('update')->andReturn(true);
$mockApplication = Mockery::mock(Application::class);
$mockApplication->shouldReceive('getAttribute')
->with('build_pack')
->andReturn('dockerfile');
$mockApplication->build_pack = 'dockerfile';
$mockSettings = Mockery::mock();
$mockSettings->shouldReceive('getAttribute')
->with('is_consistent_container_name_enabled')
->andReturn(false);
$mockSettings->shouldReceive('getAttribute')
->with('custom_internal_name')
->andReturn('');
$mockSettings->is_consistent_container_name_enabled = false;
$mockSettings->custom_internal_name = '';
$mockApplication->shouldReceive('getAttribute')
->with('settings')
->andReturn($mockSettings);
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldAllowMockingProtectedMethods();
$reflection = new \ReflectionClass($job);
$queueProperty = $reflection->getProperty('application_deployment_queue');
$queueProperty->setAccessible(true);
$queueProperty->setValue($job, $mockQueue);
$applicationProperty = $reflection->getProperty('application');
$applicationProperty->setAccessible(true);
$applicationProperty->setValue($job, $mockApplication);
$pullRequestProperty = $reflection->getProperty('pull_request_id');
$pullRequestProperty->setAccessible(true);
$pullRequestProperty->setValue($job, 0);
$job->shouldReceive('failDeployment')->andReturn();
$job->shouldReceive('execute_remote_command')->andReturn();
$failedMethod = $reflection->getMethod('failed');
$failedMethod->setAccessible(true);
$failedMethod->invoke($job, $exception);
$errorMessages = array_column($logEntries, 'message');
$errorMessageString = implode("\n", $errorMessages);
// Should log error code 0 (not skip it)
expect($errorMessageString)->toContain('Error code: 0');
});