Files
coolify/tests/Unit/DockerRemoveWithTimeoutTest.php
T
Andras BacsaiandClaude Opus 5.5 a70e856882 test: remove tests that do not test behavior and strengthen weak ones
- Remove tests that only check markup, CSS, static text, wire:key strings,
  framework behavior, copied app logic, or expect(true)
- Replace source-grep and Reflection checks of security fixes with tests
  that run the real code (shell quoting, locked properties, 403 checks,
  webhook fork trust, log redaction, Sentinel token cleanup)
- Assert response bodies and unchanged data on API error paths
- Run the Traefik version job through handle() instead of private methods
- Update stale tests for scheduled task props, the default Postgres image,
  and the disabled transfer link of unvalidated servers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:47:56 +01:00

170 lines
6.7 KiB
PHP

<?php
use App\Jobs\RemoveContainerJob;
use App\Models\Server;
use Symfony\Component\Process\Process;
it('bounds forced container removal and reports a timeout marker', function () {
expect(dockerRemoveCommandWithTimeout('container name'))
->toStartWith("bash -c '")
->toContain('timeout -k 10s 60s docker rm -f')
->toContain('__COOLIFY_CONTAINER_REMOVE_TIMEOUT__');
});
it('uses timeout syntax supported by coreutils and busybox', function () {
expect(dockerRemoveCommandWithTimeout('container-name'))
->toContain('command -v timeout')
->toContain('timeout -k 10s 60s')
->not->toContain('--kill-after');
});
it('preserves bounded cleanup when commands are adapted for non-root servers', function () {
$server = Mockery::mock(Server::class)->makePartial();
$server->shouldReceive('getAttribute')->with('user')->andReturn('ubuntu');
$server->shouldReceive('setAttribute')->andReturnSelf();
$server->user = 'ubuntu';
$commands = parseCommandsByLineForSudo(
collect([dockerRemoveCommandWithTimeout('container-name')]),
$server
);
$command = $commands[0];
expect($command)
->toStartWith("sudo bash -c '")
->toContain('timeout -k 10s 60s docker rm -f')
->toContain('__COOLIFY_CONTAINER_REMOVE_TIMEOUT__');
});
it('escapes container names in bounded removal commands', function () {
$containerName = "container'; reboot; '";
$directory = sys_get_temp_dir().'/coolify-docker-remove-'.bin2hex(random_bytes(4));
$captureFile = $directory.'/arguments';
mkdir($directory);
file_put_contents($directory.'/docker', "#!/bin/sh\nprintf '%s\\n' \"\$@\" > \"\$CAPTURE_FILE\"\n");
chmod($directory.'/docker', 0755);
$process = new Process(['/bin/sh', '-c', dockerRemoveCommandWithTimeout($containerName)], env: [
'PATH' => $directory.':'.getenv('PATH'),
'CAPTURE_FILE' => $captureFile,
]);
$process->run();
expect($process->isSuccessful())->toBeTrue()
->and(file_get_contents($captureFile))->toBe("rm\n-f\n{$containerName}\n");
unlink($captureFile);
unlink($directory.'/docker');
rmdir($directory);
});
it('succeeds when the container was already removed', function () {
$directory = sys_get_temp_dir().'/coolify-docker-remove-'.bin2hex(random_bytes(4));
mkdir($directory);
file_put_contents($directory.'/docker', "#!/bin/sh\necho 'Error response from daemon: No such container: container-name' >&2\nexit 1\n");
chmod($directory.'/docker', 0755);
$process = new Process(['/bin/sh', '-c', dockerRemoveCommandWithTimeout('container-name')], env: [
'PATH' => $directory.':'.getenv('PATH'),
]);
$process->run();
expect($process->isSuccessful())->toBeTrue();
unlink($directory.'/docker');
rmdir($directory);
});
it('reports a timeout when timeout output also says the container is missing', function () {
$directory = sys_get_temp_dir().'/coolify-docker-remove-'.bin2hex(random_bytes(4));
mkdir($directory);
file_put_contents($directory.'/timeout', "#!/bin/sh\necho 'Error response from daemon: No such container: container-name'\nexit 124\n");
chmod($directory.'/timeout', 0755);
$process = new Process(['/bin/sh', '-c', dockerRemoveCommandWithTimeout('container-name')], env: [
'PATH' => $directory.':'.getenv('PATH'),
]);
$process->run();
expect($process->getExitCode())->toBe(124)
->and($process->getOutput())->toContain('__COOLIFY_CONTAINER_REMOVE_TIMEOUT__');
unlink($directory.'/timeout');
rmdir($directory);
});
it('fails when Docker cannot remove an existing container', function () {
$directory = sys_get_temp_dir().'/coolify-docker-remove-'.bin2hex(random_bytes(4));
mkdir($directory);
file_put_contents($directory.'/docker', "#!/bin/sh\necho 'Error response from daemon: removal already in progress' >&2\nexit 1\n");
chmod($directory.'/docker', 0755);
$process = new Process(['/bin/sh', '-c', dockerRemoveCommandWithTimeout('container-name')], env: [
'PATH' => $directory.':'.getenv('PATH'),
]);
$process->run();
expect($process->isSuccessful())->toBeFalse()
->and($process->getErrorOutput())->toContain('removal already in progress');
unlink($directory.'/docker');
rmdir($directory);
});
it('makes regular container removal idempotent without hiding other failures', function () {
$directory = sys_get_temp_dir().'/coolify-docker-remove-'.bin2hex(random_bytes(4));
mkdir($directory);
file_put_contents($directory.'/docker', "#!/bin/sh\necho \"\$DOCKER_ERROR\" >&2\nexit 1\n");
chmod($directory.'/docker', 0755);
$missingContainer = new Process(['/bin/sh', '-c', dockerRemoveCommand('container name')], env: [
'PATH' => $directory.':'.getenv('PATH'),
'DOCKER_ERROR' => 'Error response from daemon: No such container: container name',
]);
$missingContainer->run();
$realFailure = new Process(['/bin/sh', '-c', dockerRemoveCommand('container name')], env: [
'PATH' => $directory.':'.getenv('PATH'),
'DOCKER_ERROR' => 'Error response from daemon: removal already in progress',
]);
$realFailure->run();
expect($missingContainer->isSuccessful())->toBeTrue()
->and($realFailure->isSuccessful())->toBeFalse();
unlink($directory.'/docker');
rmdir($directory);
});
it('makes network removal idempotent without hiding other failures', function () {
$directory = sys_get_temp_dir().'/coolify-docker-remove-'.bin2hex(random_bytes(4));
mkdir($directory);
file_put_contents($directory.'/docker', "#!/bin/sh\necho \"\$DOCKER_ERROR\" >&2\nexit 1\n");
chmod($directory.'/docker', 0755);
$missingNetwork = new Process(['/bin/sh', '-c', dockerNetworkRemoveCommand('network name')], env: [
'PATH' => $directory.':'.getenv('PATH'),
'DOCKER_ERROR' => 'Error response from daemon: network network name not found',
]);
$missingNetwork->run();
$realFailure = new Process(['/bin/sh', '-c', dockerNetworkRemoveCommand('network name')], env: [
'PATH' => $directory.':'.getenv('PATH'),
'DOCKER_ERROR' => 'Error response from daemon: network has active endpoints',
]);
$realFailure->run();
expect($missingNetwork->isSuccessful())->toBeTrue()
->and($realFailure->isSuccessful())->toBeFalse();
unlink($directory.'/docker');
rmdir($directory);
});
it('configures deferred removal attempts to outlive the shell timeout', function () {
$job = new RemoveContainerJob(123, 'container-name');
expect($job->serverId)->toBe(123)
->and($job->containerName)->toBe('container-name')
->and($job->timeout)->toBeGreaterThan(60)
->and($job->tries)->toBeGreaterThan(1);
});