Files
coolify/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php
T
Andras Bacsai 1f4006ac49 test: remove tests that do not exercise app code
Remove tests that copy app logic into the test, grep PHP source as
text, only check literals, constants, class_exists or PHP built-ins,
duplicate other tests, or are always skipped. Keep source checks that
guard security and authorization rules.

Rename a duplicate Pest helper so the full suite loads, drop the
missing v5/Browser path, and remove the CLAUDE.md symlink in favor of
AGENTS.md. Correct AGENTS.md: Dusk is not installed and the upgrade
script is scripts/upgrade.sh.
2026-09-28 11:55:56 +02:00

206 lines
8.1 KiB
PHP

<?php
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\BaseModel;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\StandalonePostgresql;
use App\Notifications\Application\RestartLimitReached;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery\MockInterface;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 0, 'fqdn' => 'https://coolify.test']);
});
function applicationWithRestartState(array $attributes = []): Application
{
$application = new Application;
$application->forceFill(array_merge([
'status' => 'exited:unhealthy',
'container_present' => true,
'restart_count' => 2,
'max_restart_count' => 2,
'restart_limit_reached' => true,
'last_restart_type' => 'crash',
'last_restart_at' => now(),
], $attributes));
return $application;
}
it('detects applications stopped after reaching the crash restart limit', function () {
expect(applicationWithRestartState()->stoppedAfterRestartLimit())->toBeTrue()
->and(applicationWithRestartState(['status' => 'running:unhealthy'])->stoppedAfterRestartLimit())->toBeFalse()
->and(applicationWithRestartState(['restart_limit_reached' => false])->stoppedAfterRestartLimit())->toBeFalse();
});
it('keeps the restart limit state after Docker resets its counter', function () {
expect(applicationWithRestartState([
'restart_count' => 0,
'last_restart_type' => null,
'last_restart_at' => null,
])->stoppedAfterRestartLimit())->toBeTrue();
});
it('preserves exited application state when the container snapshot is empty', function () {
$application = Mockery::mock(Application::class)->makePartial();
$application->setRelation('additional_servers', collect());
$application->forceFill([
'id' => 1,
'status' => 'exited:unhealthy',
'container_present' => true,
'restart_limit_reached' => true,
]);
$application->shouldNotReceive('update');
$services = Mockery::mock();
$services->shouldReceive('get')->once()->andReturn(collect());
$server = Mockery::mock(Server::class, function (MockInterface $mock) use ($application, $services) {
$mock->shouldReceive('isFunctional')->once()->andReturnTrue();
$mock->shouldReceive('applications')->once()->andReturn(collect([$application]));
$mock->shouldReceive('databases')->once()->andReturn(collect());
$mock->shouldReceive('services')->once()->andReturn($services);
$mock->shouldReceive('previews')->once()->andReturn(collect());
})->makePartial();
$server->setRelation('team', (object) ['id' => 1]);
GetContainersStatus::run($server, collect(), collect());
expect($application->container_present)->toBeTrue()
->and($application->restart_limit_reached)->toBeTrue();
});
it('does not infer the restart limit from an exited existing container', function () {
expect(applicationWithRestartState([
'container_present' => true,
'restart_limit_reached' => false,
])->stoppedAfterRestartLimit())->toBeFalse();
});
it('shows a stopped after restart limit warning badge', function () {
$html = view('components.application.restart-limit-warning', [
'application' => applicationWithRestartState(),
])->render();
expect($html)->toContain('Restart limit reached')
->not->toContain('Stopped after reaching restart limit (2/2).')
->and($html)->toContain('Container has crashed and Coolify stopped it after 2 restart attempts.');
});
it('does not show the restart limit warning for a normal manual stop', function () {
$html = view('components.application.restart-limit-warning', [
'application' => applicationWithRestartState([
'restart_count' => 0,
'last_restart_type' => null,
'restart_limit_reached' => false,
]),
])->render();
expect($html)->not->toContain('Restart limit reached');
});
it('builds restart limit notification urls from the instance base url', function () {
$application = new Application;
$application->forceFill([
'name' => 'crashy-app',
'uuid' => 'application-uuid',
'restart_count' => 2,
'max_restart_count' => 2,
]);
$application->setRelation('environment', (object) [
'uuid' => 'environment-uuid',
'name' => 'production',
'project' => (object) ['uuid' => 'project-uuid'],
]);
$notification = new RestartLimitReached($application);
expect($notification->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/application/application-uuid');
});
it('links preview, service resource and database restart limit notifications to their pages', function () {
$environment = (object) ['uuid' => 'environment-uuid', 'name' => 'production', 'project' => (object) ['uuid' => 'project-uuid']];
$application = new Application;
$application->forceFill(['name' => 'app', 'uuid' => 'application-uuid']);
$application->setRelation('environment', $environment);
$preview = new ApplicationPreview;
$preview->forceFill(['uuid' => 'preview-uuid', 'pull_request_id' => 42, 'restart_count' => 2, 'max_restart_count' => 2]);
$preview->setRelation('application', $application);
$service = new Service;
$service->forceFill(['uuid' => 'service-uuid']);
$service->setRelation('environment', $environment);
$serviceApplication = new ServiceApplication;
$serviceApplication->forceFill(['name' => 'database', 'uuid' => 'service-application-uuid', 'restart_count' => 2, 'max_restart_count' => 2]);
$serviceApplication->setRelation('service', $service);
$database = new StandalonePostgresql;
$database->forceFill(['name' => 'postgres', 'uuid' => 'database-uuid', 'restart_count' => 2, 'max_restart_count' => 2]);
$database->setRelation('environment', $environment);
expect((new RestartLimitReached($preview))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/application/application-uuid')
->and((new RestartLimitReached($serviceApplication))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/service/service-uuid')
->and((new RestartLimitReached($database))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/database/database-uuid');
});
it('uses the resolved environment project name in Slack restart limit notifications', function () {
$environment = (object) [
'uuid' => 'environment-uuid',
'name' => 'production',
'project' => (object) [
'uuid' => 'project-uuid',
'name' => 'Coolify',
],
];
$application = new class extends Application
{
public function link(): string
{
return 'https://coolify.test/application';
}
};
$application->forceFill(['name' => 'app']);
$application->setRelation('environment', $environment);
$preview = new ApplicationPreview;
$preview->forceFill([
'uuid' => 'preview-uuid',
'pull_request_id' => 42,
'restart_count' => 2,
'max_restart_count' => 2,
]);
$preview->setRelation('application', $application);
$serviceResource = new class extends BaseModel {};
$serviceResource->forceFill([
'name' => 'database',
'uuid' => 'service-resource-uuid',
'restart_count' => 2,
'max_restart_count' => 2,
]);
$serviceResource->setRelation('service', new class($environment)
{
public function __construct(public object $environment) {}
public function link(): string
{
return 'https://coolify.test/service';
}
});
expect((new RestartLimitReached($preview))->toSlack()->description)
->toContain('*Project:* Coolify')
->and((new RestartLimitReached($serviceResource))->toSlack()->description)
->toContain('*Project:* Coolify');
});