mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 12:14:07 +00:00
fix(database): refresh SSL/status state and harden clone writes
Handle database status updates more reliably by listening for `ServiceChecked` and using explicit `refresh()` handlers in Livewire database components. Also switch guarded clone/create paths to `forceFill`/`forceCreate` in helper flows to avoid missing persisted attributes during app/service cloning. Update log/terminal font stacks to Geist (with bundled variable fonts) and add coverage for SSL status refresh, persistent volume UUID cloning, and log font styling.
This commit is contained in:
1 parent
1efed14169
commit
2692496726
24 files changed
+333
-33
No files matched your search
@@ -1,15 +1,18 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\ResourceOperations;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\Environment;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\Project;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
@@ -17,7 +20,7 @@ beforeEach(function () {
|
||||
$this->user->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->destination = StandaloneDocker::factory()->create(['server_id' => $this->server->id]);
|
||||
$this->destination = $this->server->standaloneDockers()->firstOrFail();
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
|
||||
@@ -25,8 +28,13 @@ beforeEach(function () {
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'redirect' => 'both',
|
||||
]);
|
||||
|
||||
$this->application->settings->forceFill([
|
||||
'is_container_label_readonly_enabled' => false,
|
||||
])->save();
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
@@ -82,3 +90,71 @@ test('cloning application with multiple persistent volumes generates unique uuid
|
||||
expect($clonedUuids)->each->not->toBeIn($originalUuids);
|
||||
expect(array_unique($clonedUuids))->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('cloning application reassigns settings to the cloned application', function () {
|
||||
$this->application->settings->forceFill([
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
'is_build_server_enabled' => true,
|
||||
])->save();
|
||||
|
||||
$newApp = clone_application($this->application, $this->destination, [
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
$sourceSettingsCount = ApplicationSetting::query()
|
||||
->where('application_id', $this->application->id)
|
||||
->count();
|
||||
$clonedSettings = ApplicationSetting::query()
|
||||
->where('application_id', $newApp->id)
|
||||
->first();
|
||||
|
||||
expect($sourceSettingsCount)->toBe(1)
|
||||
->and($clonedSettings)->not->toBeNull()
|
||||
->and($clonedSettings?->application_id)->toBe($newApp->id)
|
||||
->and($clonedSettings?->is_static)->toBeTrue()
|
||||
->and($clonedSettings?->is_spa)->toBeTrue()
|
||||
->and($clonedSettings?->is_build_server_enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
test('cloning application reassigns scheduled tasks and previews to the cloned application', function () {
|
||||
$scheduledTask = ScheduledTask::forceCreate([
|
||||
'uuid' => 'scheduled-task-original',
|
||||
'application_id' => $this->application->id,
|
||||
'team_id' => $this->team->id,
|
||||
'name' => 'nightly-task',
|
||||
'command' => 'php artisan schedule:run',
|
||||
'frequency' => '* * * * *',
|
||||
'container' => 'app',
|
||||
'timeout' => 120,
|
||||
]);
|
||||
|
||||
$preview = ApplicationPreview::forceCreate([
|
||||
'uuid' => 'preview-original',
|
||||
'application_id' => $this->application->id,
|
||||
'pull_request_id' => 123,
|
||||
'pull_request_html_url' => 'https://example.com/pull/123',
|
||||
'fqdn' => 'https://preview.example.com',
|
||||
'status' => 'running',
|
||||
]);
|
||||
|
||||
$newApp = clone_application($this->application, $this->destination, [
|
||||
'environment_id' => $this->environment->id,
|
||||
]);
|
||||
|
||||
$clonedTask = ScheduledTask::query()
|
||||
->where('application_id', $newApp->id)
|
||||
->first();
|
||||
$clonedPreview = ApplicationPreview::query()
|
||||
->where('application_id', $newApp->id)
|
||||
->first();
|
||||
|
||||
expect($clonedTask)->not->toBeNull()
|
||||
->and($clonedTask?->uuid)->not->toBe($scheduledTask->uuid)
|
||||
->and($clonedTask?->application_id)->toBe($newApp->id)
|
||||
->and($clonedTask?->team_id)->toBe($this->team->id)
|
||||
->and($clonedPreview)->not->toBeNull()
|
||||
->and($clonedPreview?->uuid)->not->toBe($preview->uuid)
|
||||
->and($clonedPreview?->application_id)->toBe($newApp->id)
|
||||
->and($clonedPreview?->status)->toBe('exited');
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Database\Dragonfly\General as DragonflyGeneral;
|
||||
use App\Livewire\Project\Database\Keydb\General as KeydbGeneral;
|
||||
use App\Livewire\Project\Database\Mariadb\General as MariadbGeneral;
|
||||
use App\Livewire\Project\Database\Mongodb\General as MongodbGeneral;
|
||||
use App\Livewire\Project\Database\Mysql\General as MysqlGeneral;
|
||||
use App\Livewire\Project\Database\Postgresql\General as PostgresqlGeneral;
|
||||
use App\Livewire\Project\Database\Redis\General as RedisGeneral;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
dataset('ssl-aware-database-general-components', [
|
||||
MysqlGeneral::class,
|
||||
MariadbGeneral::class,
|
||||
MongodbGeneral::class,
|
||||
RedisGeneral::class,
|
||||
PostgresqlGeneral::class,
|
||||
KeydbGeneral::class,
|
||||
DragonflyGeneral::class,
|
||||
]);
|
||||
|
||||
it('maps database status broadcasts to refresh for ssl-aware database general components', function (string $componentClass) {
|
||||
$component = app($componentClass);
|
||||
$listeners = $component->getListeners();
|
||||
|
||||
expect($listeners["echo-private:user.{$this->user->id},DatabaseStatusChanged"])->toBe('refresh')
|
||||
->and($listeners["echo-private:team.{$this->team->id},ServiceChecked"])->toBe('refresh');
|
||||
})->with('ssl-aware-database-general-components');
|
||||
|
||||
it('reloads the mysql database model when refreshing so ssl controls follow the latest status', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
$database = StandaloneMysql::forceCreate([
|
||||
'name' => 'test-mysql',
|
||||
'image' => 'mysql:8',
|
||||
'mysql_root_password' => 'password',
|
||||
'mysql_user' => 'coolify',
|
||||
'mysql_password' => 'password',
|
||||
'mysql_database' => 'coolify',
|
||||
'status' => 'exited:unhealthy',
|
||||
'enable_ssl' => true,
|
||||
'is_log_drain_enabled' => false,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$component = Livewire::test(MysqlGeneral::class, ['database' => $database])
|
||||
->assertDontSee('Database should be stopped to change this settings.');
|
||||
|
||||
$database->forceFill(['status' => 'running:healthy'])->save();
|
||||
|
||||
$component->call('refresh')
|
||||
->assertSee('Database should be stopped to change this settings.');
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
it('registers geist mono from a local asset for log surfaces', function () {
|
||||
$fontsCss = file_get_contents(resource_path('css/fonts.css'));
|
||||
$appCss = file_get_contents(resource_path('css/app.css'));
|
||||
$fontPath = resource_path('fonts/geist-mono-variable.woff2');
|
||||
$geistSansPath = resource_path('fonts/geist-sans-variable.woff2');
|
||||
|
||||
expect($fontsCss)
|
||||
->toContain("font-family: 'Geist Mono'")
|
||||
->toContain("url('../fonts/geist-mono-variable.woff2')")
|
||||
->toContain("font-family: 'Geist Sans'")
|
||||
->toContain("url('../fonts/geist-sans-variable.woff2')")
|
||||
->and($appCss)
|
||||
->toContain("--font-sans: 'Geist Sans', Inter, sans-serif")
|
||||
->toContain('@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;')
|
||||
->toContain("--font-logs: 'Geist Mono'")
|
||||
->toContain("--font-geist-sans: 'Geist Sans'")
|
||||
->and($fontPath)
|
||||
->toBeFile()
|
||||
->and($geistSansPath)
|
||||
->toBeFile();
|
||||
});
|
||||
|
||||
it('uses geist mono for shared logs and terminal rendering', function () {
|
||||
$sharedLogsView = file_get_contents(resource_path('views/livewire/project/shared/get-logs.blade.php'));
|
||||
$deploymentLogsView = file_get_contents(resource_path('views/livewire/project/application/deployment/show.blade.php'));
|
||||
$activityMonitorView = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php'));
|
||||
$dockerCleanupView = file_get_contents(resource_path('views/livewire/server/docker-cleanup-executions.blade.php'));
|
||||
$terminalClient = file_get_contents(resource_path('js/terminal.js'));
|
||||
|
||||
expect($sharedLogsView)
|
||||
->toContain('class="font-logs max-w-full cursor-default"')
|
||||
->toContain('class="font-logs whitespace-pre-wrap break-all max-w-full text-neutral-400"')
|
||||
->and($deploymentLogsView)
|
||||
->toContain('class="flex flex-col font-logs"')
|
||||
->toContain('class="font-logs text-neutral-400 mb-2"')
|
||||
->and($activityMonitorView)
|
||||
->toContain('<pre class="font-logs whitespace-pre-wrap"')
|
||||
->and($dockerCleanupView)
|
||||
->toContain('class="flex-1 text-sm font-logs text-gray-700 dark:text-gray-300"')
|
||||
->toContain('class="font-logs text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap"')
|
||||
->and($terminalClient)
|
||||
->toContain('"Geist Mono"');
|
||||
});
|
||||
@@ -41,7 +41,8 @@ it('ensures parseDockerComposeFile does not create duplicates on null savedServi
|
||||
// The new code checks for null within the else block and creates only if needed
|
||||
expect($sharedFile)
|
||||
->toContain('if (is_null($savedService)) {')
|
||||
->toContain('$savedService = ServiceDatabase::create([');
|
||||
->toContain('$savedService = ServiceDatabase::forceCreate([')
|
||||
->toContain('$savedService = ServiceApplication::forceCreate([');
|
||||
});
|
||||
|
||||
it('verifies image update logic is present in parseDockerComposeFile', function () {
|
||||
|
||||
Reference in new issue
Block a user