mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-26 02:19:58 +00:00
Merge branch 'next' into improve-scheduled-tasks
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Unit tests for PORT environment variable detection feature.
|
||||
*
|
||||
* Tests verify that the Application model can correctly detect PORT environment
|
||||
* variables and provide information to the UI about matches and mismatches with
|
||||
* the configured ports_exposes field.
|
||||
*/
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Mockery;
|
||||
|
||||
beforeEach(function () {
|
||||
// Clean up Mockery after each test
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
it('detects PORT environment variable when present', function () {
|
||||
// Create a mock Application instance
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
|
||||
// Mock environment variables collection with PORT set to 3000
|
||||
$portEnvVar = Mockery::mock(EnvironmentVariable::class);
|
||||
$portEnvVar->shouldReceive('getAttribute')->with('real_value')->andReturn('3000');
|
||||
|
||||
$envVars = new Collection([$portEnvVar]);
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('environment_variables')
|
||||
->andReturn($envVars);
|
||||
|
||||
// Mock the firstWhere method to return our PORT env var
|
||||
$envVars = Mockery::mock(Collection::class);
|
||||
$envVars->shouldReceive('firstWhere')->with('key', 'PORT')->andReturn($portEnvVar);
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('environment_variables')
|
||||
->andReturn($envVars);
|
||||
|
||||
// Call the method we're testing
|
||||
$detectedPort = $application->detectPortFromEnvironment();
|
||||
|
||||
expect($detectedPort)->toBe(3000);
|
||||
});
|
||||
|
||||
it('returns null when PORT environment variable is not set', function () {
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
|
||||
// Mock environment variables collection without PORT
|
||||
$envVars = Mockery::mock(Collection::class);
|
||||
$envVars->shouldReceive('firstWhere')->with('key', 'PORT')->andReturn(null);
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('environment_variables')
|
||||
->andReturn($envVars);
|
||||
|
||||
$detectedPort = $application->detectPortFromEnvironment();
|
||||
|
||||
expect($detectedPort)->toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when PORT value is not numeric', function () {
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
|
||||
// Mock environment variables with non-numeric PORT value
|
||||
$portEnvVar = Mockery::mock(EnvironmentVariable::class);
|
||||
$portEnvVar->shouldReceive('getAttribute')->with('real_value')->andReturn('invalid-port');
|
||||
|
||||
$envVars = Mockery::mock(Collection::class);
|
||||
$envVars->shouldReceive('firstWhere')->with('key', 'PORT')->andReturn($portEnvVar);
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('environment_variables')
|
||||
->andReturn($envVars);
|
||||
|
||||
$detectedPort = $application->detectPortFromEnvironment();
|
||||
|
||||
expect($detectedPort)->toBeNull();
|
||||
});
|
||||
|
||||
it('handles PORT value with whitespace', function () {
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
|
||||
// Mock environment variables with PORT value that has whitespace
|
||||
$portEnvVar = Mockery::mock(EnvironmentVariable::class);
|
||||
$portEnvVar->shouldReceive('getAttribute')->with('real_value')->andReturn(' 8080 ');
|
||||
|
||||
$envVars = Mockery::mock(Collection::class);
|
||||
$envVars->shouldReceive('firstWhere')->with('key', 'PORT')->andReturn($portEnvVar);
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('environment_variables')
|
||||
->andReturn($envVars);
|
||||
|
||||
$detectedPort = $application->detectPortFromEnvironment();
|
||||
|
||||
expect($detectedPort)->toBe(8080);
|
||||
});
|
||||
|
||||
it('detects PORT from preview environment variables when isPreview is true', function () {
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
|
||||
// Mock preview environment variables with PORT
|
||||
$portEnvVar = Mockery::mock(EnvironmentVariable::class);
|
||||
$portEnvVar->shouldReceive('getAttribute')->with('real_value')->andReturn('4000');
|
||||
|
||||
$envVars = Mockery::mock(Collection::class);
|
||||
$envVars->shouldReceive('firstWhere')->with('key', 'PORT')->andReturn($portEnvVar);
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('environment_variables_preview')
|
||||
->andReturn($envVars);
|
||||
|
||||
$detectedPort = $application->detectPortFromEnvironment(true);
|
||||
|
||||
expect($detectedPort)->toBe(4000);
|
||||
});
|
||||
|
||||
it('verifies ports_exposes array conversion logic', function () {
|
||||
// Test the logic that converts comma-separated ports to array
|
||||
$portsExposesString = '3000,3001,8080';
|
||||
$expectedArray = [3000, 3001, 8080];
|
||||
|
||||
// This simulates what portsExposesArray accessor does
|
||||
$result = is_null($portsExposesString)
|
||||
? []
|
||||
: explode(',', $portsExposesString);
|
||||
|
||||
// Convert to integers for comparison
|
||||
$result = array_map('intval', $result);
|
||||
|
||||
expect($result)->toBe($expectedArray);
|
||||
});
|
||||
|
||||
it('verifies PORT matches detection logic', function () {
|
||||
$detectedPort = 3000;
|
||||
$portsExposesArray = [3000, 3001];
|
||||
|
||||
$isMatch = in_array($detectedPort, $portsExposesArray);
|
||||
|
||||
expect($isMatch)->toBeTrue();
|
||||
});
|
||||
|
||||
it('verifies PORT mismatch detection logic', function () {
|
||||
$detectedPort = 8080;
|
||||
$portsExposesArray = [3000, 3001];
|
||||
|
||||
$isMatch = in_array($detectedPort, $portsExposesArray);
|
||||
|
||||
expect($isMatch)->toBeFalse();
|
||||
});
|
||||
|
||||
it('verifies empty ports_exposes detection logic', function () {
|
||||
$portsExposesArray = [];
|
||||
|
||||
$isEmpty = empty($portsExposesArray);
|
||||
|
||||
expect($isEmpty)->toBeTrue();
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
|
||||
beforeEach(function () {
|
||||
// Mock server
|
||||
$this->server = Mockery::mock(Server::class);
|
||||
$this->server->shouldReceive('isFunctional')->andReturn(true);
|
||||
$this->server->shouldReceive('isSwarm')->andReturn(false);
|
||||
$this->server->shouldReceive('applications')->andReturn(collect());
|
||||
|
||||
// Mock application
|
||||
$this->application = Mockery::mock(Application::class);
|
||||
$this->application->shouldReceive('getAttribute')->with('id')->andReturn(1);
|
||||
$this->application->shouldReceive('getAttribute')->with('name')->andReturn('test-app');
|
||||
$this->application->shouldReceive('getAttribute')->with('restart_count')->andReturn(0);
|
||||
$this->application->shouldReceive('getAttribute')->with('uuid')->andReturn('test-uuid');
|
||||
$this->application->shouldReceive('getAttribute')->with('environment')->andReturn(null);
|
||||
});
|
||||
|
||||
it('extracts restart count from container data', function () {
|
||||
$containerData = [
|
||||
'RestartCount' => 5,
|
||||
'State' => [
|
||||
'Status' => 'running',
|
||||
'Health' => ['Status' => 'healthy'],
|
||||
],
|
||||
'Config' => [
|
||||
'Labels' => [
|
||||
'coolify.applicationId' => '1',
|
||||
'com.docker.compose.service' => 'web',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$restartCount = data_get($containerData, 'RestartCount', 0);
|
||||
|
||||
expect($restartCount)->toBe(5);
|
||||
});
|
||||
|
||||
it('defaults to zero when restart count is missing', function () {
|
||||
$containerData = [
|
||||
'State' => [
|
||||
'Status' => 'running',
|
||||
],
|
||||
'Config' => [
|
||||
'Labels' => [],
|
||||
],
|
||||
];
|
||||
|
||||
$restartCount = data_get($containerData, 'RestartCount', 0);
|
||||
|
||||
expect($restartCount)->toBe(0);
|
||||
});
|
||||
|
||||
it('detects restart count increase', function () {
|
||||
$previousRestartCount = 2;
|
||||
$currentRestartCount = 5;
|
||||
|
||||
expect($currentRestartCount)->toBeGreaterThan($previousRestartCount);
|
||||
});
|
||||
|
||||
it('identifies maximum restart count from multiple containers', function () {
|
||||
$containerRestartCounts = collect([
|
||||
'web' => 3,
|
||||
'worker' => 5,
|
||||
'scheduler' => 1,
|
||||
]);
|
||||
|
||||
$maxRestartCount = $containerRestartCounts->max();
|
||||
|
||||
expect($maxRestartCount)->toBe(5);
|
||||
});
|
||||
|
||||
it('handles empty restart counts collection', function () {
|
||||
$containerRestartCounts = collect([]);
|
||||
|
||||
$maxRestartCount = $containerRestartCounts->max() ?? 0;
|
||||
|
||||
expect($maxRestartCount)->toBe(0);
|
||||
});
|
||||
@@ -194,6 +194,36 @@ YAML;
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('array-format with environment variable and path concatenation', function () {
|
||||
// This is the reported issue #7127 - ${VAR}/path should be allowed
|
||||
$dockerComposeYaml = <<<'YAML'
|
||||
services:
|
||||
web:
|
||||
image: nginx
|
||||
volumes:
|
||||
- type: bind
|
||||
source: '${VOLUMES_PATH}/mysql'
|
||||
target: /var/lib/mysql
|
||||
- type: bind
|
||||
source: '${DATA_PATH}/config'
|
||||
target: /etc/config
|
||||
- type: bind
|
||||
source: '${VOLUME_PATH}/app_data'
|
||||
target: /app/data
|
||||
YAML;
|
||||
|
||||
$parsed = Yaml::parse($dockerComposeYaml);
|
||||
|
||||
// Verify all three volumes have the correct source format
|
||||
expect($parsed['services']['web']['volumes'][0]['source'])->toBe('${VOLUMES_PATH}/mysql');
|
||||
expect($parsed['services']['web']['volumes'][1]['source'])->toBe('${DATA_PATH}/config');
|
||||
expect($parsed['services']['web']['volumes'][2]['source'])->toBe('${VOLUME_PATH}/app_data');
|
||||
|
||||
// The validation should allow this - the reported bug was that it was blocked
|
||||
expect(fn () => validateDockerComposeForInjection($dockerComposeYaml))
|
||||
->not->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('array-format with malicious environment variable default', function () {
|
||||
$dockerComposeYaml = <<<'YAML'
|
||||
services:
|
||||
|
||||
@@ -94,6 +94,27 @@ test('parseDockerVolumeString accepts simple environment variables', function ()
|
||||
}
|
||||
});
|
||||
|
||||
test('parseDockerVolumeString accepts environment variables with path concatenation', function () {
|
||||
$volumes = [
|
||||
'${VOLUMES_PATH}/mysql:/var/lib/mysql',
|
||||
'${DATA_PATH}/config:/etc/config',
|
||||
'${VOLUME_PATH}/app_data:/app',
|
||||
'${MY_VAR_123}/deep/nested/path:/data',
|
||||
'${VAR}/path:/app',
|
||||
'${VAR}_suffix:/app',
|
||||
'${VAR}-suffix:/app',
|
||||
'${VAR}.ext:/app',
|
||||
'${VOLUMES_PATH}/mysql:/var/lib/mysql:ro',
|
||||
'${DATA_PATH}/config:/etc/config:rw',
|
||||
];
|
||||
|
||||
foreach ($volumes as $volume) {
|
||||
$result = parseDockerVolumeString($volume);
|
||||
expect($result)->toBeArray();
|
||||
expect($result['source'])->not->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test('parseDockerVolumeString rejects environment variables with command injection in default', function () {
|
||||
$maliciousVolumes = [
|
||||
'${VAR:-`whoami`}:/app',
|
||||
|
||||
Reference in New Issue
Block a user